Fix some portability problems with the python tests for obliterate which I
[svnrdump.git] / svntest / objects.py
blob68f141b993a620efa855c8642465e21ef69eb253
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(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", "-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 crude_bdb_parse(line):
66 """Replace BDB ([klen] kval) syntax with (kval) syntax. This is a very
67 crude parser, only just good enough to make a more human-friendly output
68 in common cases. The result is not intended to be unambiguous or machine-
69 parsable."""
70 lparts = line.split('(')
71 new_lparts = []
72 for lpart in lparts:
73 rparts = lpart.split(')')
74 new_rparts = []
75 for rpart in rparts:
76 words = rpart.split(' ')
78 # Set new_words to all the words that we want to keep
79 new_words = []
80 last_len = None
81 last_word = None
82 for word in words:
83 if last_len is None:
84 try:
85 last_len = int(word)
86 last_word = word
87 except ValueError:
88 if last_word is not None:
89 new_words += [last_word]
90 new_words += [word]
91 last_word = None
92 last_len = None
93 else: # print a word that was previously prefixed by its len
94 if not len(word) == last_len + word.count('\\') * 2:
95 print "## parsed wrongly: %d %d '%s'" % (last_len, len(word), word)
96 if word == "":
97 new_words += ["''"]
98 else:
99 new_words += [word]
100 last_word = None
101 last_len = None
103 if last_word is not None:
104 new_words += [last_word]
106 new_rparts += [' '.join(new_words)]
107 new_lparts += [')'.join(new_rparts)]
108 new_line = '('.join(new_lparts)
109 return new_line
111 def dump_bdb(repo_path, dump_dir):
112 """Dump all the known BDB tables in the repository at REPO_PATH into a
113 single text file in DUMP_DIR. Omit any "next-key" records."""
114 dump_file = dump_dir + "/all.bdb"
115 file = open(dump_file, 'w')
116 for table in ['revisions', 'transactions', 'changes', 'copies', 'nodes',
117 'node-origins', 'representations', 'checksum-reps', 'strings',
118 'locks', 'lock-tokens', 'miscellaneous', 'uuids']:
119 file.write(table + ":\n")
120 for line in db_dump(repo_path, table):
121 if line == " next-key\n":
122 break
123 file.write(crude_bdb_parse(line))
124 file.write("\n")
125 file.close()
127 # Dump the svn view of the repository
128 #svnadmin dump repo_path > dump_dir + "/dump.svn"
129 #svnadmin lslocks repo_path > dump_dir + "/locks.svn"
130 #svnadmin lstxns repo_path > dump_dir + "/txns.svn"
133 ######################################################################
134 # Class SvnRepository
136 class SvnRepository:
137 """An object of class SvnRepository represents a Subversion repository,
138 providing both a client-side view and a server-side view."""
140 def __init__(self, repo_url, repo_dir):
141 self.repo_url = repo_url
142 self.repo_absdir = os.path.abspath(repo_dir)
144 def dump(self, output_dir):
145 """Dump the repository into the directory OUTPUT_DIR"""
146 ldir = local_path(output_dir)
147 os.mkdir(ldir)
148 print "## SvnRepository::dump(rep_dir=" + self.repo_absdir + ")"
150 """Run a BDB dump on the repository"""
151 #subprocess.call(["/home/julianfoad/bin/svn-dump-bdb", self.repo_absdir, ldir])
152 dump_bdb(self.repo_absdir, ldir)
154 """Run 'svnadmin dump' on the repository."""
155 exit_code, stdout, stderr = \
156 actions.run_and_verify_svnadmin(None, None, None,
157 'dump', self.repo_absdir)
158 ldumpfile = local_path(output_dir + "/svnadmin.dump")
159 main.file_write(ldumpfile, ''.join(stderr))
160 main.file_append(ldumpfile, ''.join(stdout))
163 def obliterate_node_rev(self, path, rev):
164 """Obliterate the single node-rev PATH in revision REV."""
165 actions.run_and_verify_svn(None, None, [],
166 'obliterate',
167 self.repo_url + '/' + path + '@' + str(rev))
170 ######################################################################
171 # Class SvnWC
173 class SvnWC:
174 """An object of class SvnWC represents a WC, and provides methods for
175 operating on it. It keeps track of the state of the WC and of the
176 repository, so that the expected results of common operations are
177 automatically known.
179 Path arguments to class methods paths are relative to the WC dir and
180 in Subversion canonical form ('/' separators).
183 def __init__(self, wc_dir):
184 """Initialize the object to use the existing WC at path WC_DIR.
186 self.wc_absdir = os.path.abspath(wc_dir)
187 # 'state' is, at all times, the 'wc.State' representation of the state
188 # of the WC, with paths relative to 'wc_absdir'.
189 #self.state = wc.State('', {})
190 initial_wc_tree = tree.build_tree_from_wc(self.wc_absdir, load_props=True)
191 self.state = initial_wc_tree.as_state()
192 self.state.add({
193 '': wc.StateItem()
195 # This WC's idea of the repository's head revision.
196 # ### Shouldn't be in this class: should ask the repository instead.
197 self.head_rev = 0
199 print "## new SvnWC:"
200 print self
202 def __str__(self):
203 return "SvnWC(head_rev=" + str(self.head_rev) + ", state={" + \
204 str(self.state.desc) + \
205 "})"
207 def svn_mkdir(self, rpath):
208 lpath = local_path(rpath)
209 actions.run_and_verify_svn(None, None, [], 'mkdir', lpath)
211 self.state.add({
212 rpath : wc.StateItem(status='A ')
215 # def propset(self, pname, pvalue, *rpaths):
216 # "Set property 'pname' to value 'pvalue' on each path in 'rpaths'"
217 # local_paths = tuple([local_path(rpath) for rpath in rpaths])
218 # actions.run_and_verify_svn(None, None, [], 'propset', pname, pvalue,
219 # *local_paths)
221 def svn_set_props(self, rpath, props):
222 """Change the properties of PATH to be the dictionary {name -> value} PROPS.
224 lpath = local_path(rpath)
225 #for prop in path's existing props:
226 # actions.run_and_verify_svn(None, None, [], 'propdel',
227 # prop, lpath)
228 for prop in props:
229 actions.run_and_verify_svn(None, None, [], 'propset',
230 prop, props[prop], lpath)
231 self.state.tweak(rpath, props=props)
233 def svn_file_create_add(self, rpath, content=None, props=None):
234 "Make and add a file with some default content, and keyword expansion."
235 lpath = local_path(rpath)
236 ldirname, filename = os.path.split(lpath)
237 if content is None:
238 # Default content
239 content = "This is the file '" + filename + "'.\n" + \
240 "Last changed in '$Revision$'.\n"
241 main.file_write(lpath, content)
242 actions.run_and_verify_svn(None, None, [], 'add', lpath)
244 self.state.add({
245 rpath : wc.StateItem(status='A ')
247 if props is None:
248 # Default props
249 props = {
250 'svn:keywords': 'Revision'
252 self.svn_set_props(rpath, props)
254 def file_modify(self, rpath, content=None, props=None):
255 "Make text and property mods to a WC file."
256 lpath = local_path(rpath)
257 if content is not None:
258 #main.file_append(lpath, "An extra line.\n")
259 #actions.run_and_verify_svn(None, None, [], 'propset',
260 # 'newprop', 'v', lpath)
261 main.file_write(lpath, content)
262 self.state.tweak(rpath, content=content)
263 if props is not None:
264 self.set_props(rpath, props)
265 self.state.tweak(rpath, props=props)
267 def svn_move(self, rpath1, rpath2, parents=False):
268 """Move/rename the existing WC item RPATH1 to become RPATH2.
269 RPATH2 must not already exist. If PARENTS is true, any missing parents
270 of RPATH2 will be created."""
271 lpath1 = local_path(rpath1)
272 lpath2 = local_path(rpath2)
273 args = [lpath1, lpath2]
274 if parents:
275 args += ['--parents']
276 actions.run_and_verify_svn(None, None, [], 'copy', *args)
277 self.state.add({
278 rpath2: self.state.desc[rpath1]
280 self.state.remove(rpath1)
282 def svn_copy(self, rpath1, rpath2, parents=False, rev=None):
283 """Copy the existing WC item RPATH1 to become RPATH2.
284 RPATH2 must not already exist. If PARENTS is true, any missing parents
285 of RPATH2 will be created. If REV is not None, copy revision REV of
286 the node identified by WC item RPATH1."""
287 lpath1 = local_path(rpath1)
288 lpath2 = local_path(rpath2)
289 args = [lpath1, lpath2]
290 if rev is not None:
291 args += ['-r', rev]
292 if parents:
293 args += ['--parents']
294 actions.run_and_verify_svn(None, None, [], 'copy', *args)
295 self.state.add({
296 rpath2: self.state.desc[rpath1]
299 def svn_delete(self, rpath):
300 "Delete a WC path locally."
301 lpath = local_path(rpath)
302 actions.run_and_verify_svn(None, None, [], 'delete', lpath)
304 def svn_commit(self, rpath='', log=''):
305 "Commit a WC path (recursively). Return the new revision number."
306 lpath = local_path(rpath)
307 actions.run_and_verify_svn(None, verify.AnyOutput, [],
308 'commit', '-m', log, lpath)
309 actions.run_and_verify_update(lpath, None, None, None)
310 self.head_rev += 1
311 print "## head-rev == " + str(self.head_rev)
312 return self.head_rev
314 # def svn_merge(self, rev_spec, source, target, exp_out=None):
315 # """Merge a single change from path 'source' to path 'target'.
316 # SRC_CHANGE_NUM is either a number (to cherry-pick that specific change)
317 # or a command-line option revision range string such as '-r10:20'."""
318 # lsource = local_path(source)
319 # ltarget = local_path(target)
320 # if isinstance(rev_spec, int):
321 # rev_spec = '-c' + str(rev_spec)
322 # if exp_out is None:
323 # target_re = re.escape(target)
324 # exp_1 = "--- Merging r.* into '" + target_re + ".*':"
325 # exp_2 = "(A |D |[UG] | [UG]|[UG][UG]) " + target_re + ".*"
326 # exp_out = verify.RegexOutput(exp_1 + "|" + exp_2)
327 # actions.run_and_verify_svn(None, exp_out, [],
328 # 'merge', rev_spec, lsource, ltarget)