##// END OF EJS Templates
testing: add a 'continuous' profile...
David R. MacIver -
r28259:7829d0ba default
parent child Browse files
Show More
@@ -1,565 +1,593 b''
1 from __future__ import print_function, absolute_import
1 from __future__ import print_function, absolute_import
2
2
3 """Fuzz testing for operations against a Mercurial repository
3 """Fuzz testing for operations against a Mercurial repository
4
4
5 This uses Hypothesis's stateful testing to generate random repository
5 This uses Hypothesis's stateful testing to generate random repository
6 operations and test Mercurial using them, both to see if there are any
6 operations and test Mercurial using them, both to see if there are any
7 unexpected errors and to compare different versions of it."""
7 unexpected errors and to compare different versions of it."""
8
8
9 import os
9 import os
10 import sys
10 import sys
11
11
12 # These tests require Hypothesis and pytz to be installed.
12 # These tests require Hypothesis and pytz to be installed.
13 # Running 'pip install hypothesis pytz' will achieve that.
13 # Running 'pip install hypothesis pytz' will achieve that.
14 # Note: This won't work if you're running Python < 2.7.
14 # Note: This won't work if you're running Python < 2.7.
15 try:
15 try:
16 from hypothesis.extra.datetime import datetimes
16 from hypothesis.extra.datetime import datetimes
17 except ImportError:
17 except ImportError:
18 sys.stderr.write("skipped: hypothesis or pytz not installed" + os.linesep)
18 sys.stderr.write("skipped: hypothesis or pytz not installed" + os.linesep)
19 sys.exit(80)
19 sys.exit(80)
20
20
21 # If you are running an old version of pip you may find that the enum34
21 # If you are running an old version of pip you may find that the enum34
22 # backport is not installed automatically. If so 'pip install enum34' will
22 # backport is not installed automatically. If so 'pip install enum34' will
23 # fix this problem.
23 # fix this problem.
24 try:
24 try:
25 import enum
25 import enum
26 assert enum # Silence pyflakes
26 assert enum # Silence pyflakes
27 except ImportError:
27 except ImportError:
28 sys.stderr.write("skipped: enum34 not installed" + os.linesep)
28 sys.stderr.write("skipped: enum34 not installed" + os.linesep)
29 sys.exit(80)
29 sys.exit(80)
30
30
31 import binascii
31 import binascii
32 from contextlib import contextmanager
32 from contextlib import contextmanager
33 import errno
33 import errno
34 import pipes
34 import pipes
35 import shutil
35 import shutil
36 import silenttestrunner
36 import silenttestrunner
37 import subprocess
37 import subprocess
38
38
39 from hypothesis.errors import HypothesisException
39 from hypothesis.errors import HypothesisException
40 from hypothesis.stateful import (
40 from hypothesis.stateful import (
41 rule, RuleBasedStateMachine, Bundle, precondition)
41 rule, RuleBasedStateMachine, Bundle, precondition)
42 from hypothesis import settings, note, strategies as st
42 from hypothesis import settings, note, strategies as st
43 from hypothesis.configuration import set_hypothesis_home_dir
43 from hypothesis.configuration import set_hypothesis_home_dir
44 from hypothesis.database import ExampleDatabase
44
45
45 testdir = os.path.abspath(os.environ["TESTDIR"])
46 testdir = os.path.abspath(os.environ["TESTDIR"])
46
47
47 # We store Hypothesis examples here rather in the temporary test directory
48 # We store Hypothesis examples here rather in the temporary test directory
48 # so that when rerunning a failing test this always results in refinding the
49 # so that when rerunning a failing test this always results in refinding the
49 # previous failure. This directory is in .hgignore and should not be checked in
50 # previous failure. This directory is in .hgignore and should not be checked in
50 # but is useful to have for development.
51 # but is useful to have for development.
51 set_hypothesis_home_dir(os.path.join(testdir, ".hypothesis"))
52 set_hypothesis_home_dir(os.path.join(testdir, ".hypothesis"))
52
53
53 runtests = os.path.join(os.environ["RUNTESTDIR"], "run-tests.py")
54 runtests = os.path.join(os.environ["RUNTESTDIR"], "run-tests.py")
54 testtmp = os.environ["TESTTMP"]
55 testtmp = os.environ["TESTTMP"]
55 assert os.path.isdir(testtmp)
56 assert os.path.isdir(testtmp)
56
57
57 generatedtests = os.path.join(testdir, "hypothesis-generated")
58 generatedtests = os.path.join(testdir, "hypothesis-generated")
58
59
59 try:
60 try:
60 os.makedirs(generatedtests)
61 os.makedirs(generatedtests)
61 except OSError:
62 except OSError:
62 pass
63 pass
63
64
64 # We write out generated .t files to a file in order to ease debugging and to
65 # We write out generated .t files to a file in order to ease debugging and to
65 # give a starting point for turning failures Hypothesis finds into normal
66 # give a starting point for turning failures Hypothesis finds into normal
66 # tests. In order to ensure that multiple copies of this test can be run in
67 # tests. In order to ensure that multiple copies of this test can be run in
67 # parallel we use atomic file create to ensure that we always get a unique
68 # parallel we use atomic file create to ensure that we always get a unique
68 # name.
69 # name.
69 file_index = 0
70 file_index = 0
70 while True:
71 while True:
71 file_index += 1
72 file_index += 1
72 savefile = os.path.join(generatedtests, "test-generated-%d.t" % (
73 savefile = os.path.join(generatedtests, "test-generated-%d.t" % (
73 file_index,
74 file_index,
74 ))
75 ))
75 try:
76 try:
76 os.close(os.open(savefile, os.O_CREAT | os.O_EXCL | os.O_WRONLY))
77 os.close(os.open(savefile, os.O_CREAT | os.O_EXCL | os.O_WRONLY))
77 break
78 break
78 except OSError as e:
79 except OSError as e:
79 if e.errno != errno.EEXIST:
80 if e.errno != errno.EEXIST:
80 raise
81 raise
81 assert os.path.exists(savefile)
82 assert os.path.exists(savefile)
82
83
83 hgrc = os.path.join(".hg", "hgrc")
84 hgrc = os.path.join(".hg", "hgrc")
84
85
85 filecharacters = (
86 filecharacters = (
86 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
87 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
87 "[]^_`;=@{}~ !#$%&'()+,-"
88 "[]^_`;=@{}~ !#$%&'()+,-"
88 )
89 )
89
90
90 files = st.text(filecharacters, min_size=1).map(lambda x: x.strip()).filter(
91 files = st.text(filecharacters, min_size=1).map(lambda x: x.strip()).filter(
91 bool).map(lambda s: s.encode('ascii'))
92 bool).map(lambda s: s.encode('ascii'))
92
93
93 safetext = st.text(st.characters(
94 safetext = st.text(st.characters(
94 min_codepoint=1, max_codepoint=127,
95 min_codepoint=1, max_codepoint=127,
95 blacklist_categories=('Cc', 'Cs')), min_size=1).map(
96 blacklist_categories=('Cc', 'Cs')), min_size=1).map(
96 lambda s: s.encode('utf-8')
97 lambda s: s.encode('utf-8')
97 )
98 )
98
99
99 @contextmanager
100 @contextmanager
100 def acceptableerrors(*args):
101 def acceptableerrors(*args):
101 """Sometimes we know an operation we're about to perform might fail, and
102 """Sometimes we know an operation we're about to perform might fail, and
102 we're OK with some of the failures. In those cases this may be used as a
103 we're OK with some of the failures. In those cases this may be used as a
103 context manager and will swallow expected failures, as identified by
104 context manager and will swallow expected failures, as identified by
104 substrings of the error message Mercurial emits."""
105 substrings of the error message Mercurial emits."""
105 try:
106 try:
106 yield
107 yield
107 except subprocess.CalledProcessError as e:
108 except subprocess.CalledProcessError as e:
108 if not any(a in e.output for a in args):
109 if not any(a in e.output for a in args):
109 note(e.output)
110 note(e.output)
110 raise
111 raise
111
112
112 reponames = st.text("abcdefghijklmnopqrstuvwxyz01234556789", min_size=1).map(
113 reponames = st.text("abcdefghijklmnopqrstuvwxyz01234556789", min_size=1).map(
113 lambda s: s.encode('ascii')
114 lambda s: s.encode('ascii')
114 )
115 )
115
116
116 class verifyingstatemachine(RuleBasedStateMachine):
117 class verifyingstatemachine(RuleBasedStateMachine):
117 """This defines the set of acceptable operations on a Mercurial repository
118 """This defines the set of acceptable operations on a Mercurial repository
118 using Hypothesis's RuleBasedStateMachine.
119 using Hypothesis's RuleBasedStateMachine.
119
120
120 The general concept is that we manage multiple repositories inside a
121 The general concept is that we manage multiple repositories inside a
121 repos/ directory in our temporary test location. Some of these are freshly
122 repos/ directory in our temporary test location. Some of these are freshly
122 inited, some are clones of the others. Our current working directory is
123 inited, some are clones of the others. Our current working directory is
123 always inside one of these repositories while the tests are running.
124 always inside one of these repositories while the tests are running.
124
125
125 Hypothesis then performs a series of operations against these repositories,
126 Hypothesis then performs a series of operations against these repositories,
126 including hg commands, generating contents and editing the .hgrc file.
127 including hg commands, generating contents and editing the .hgrc file.
127 If these operations fail in unexpected ways or behave differently in
128 If these operations fail in unexpected ways or behave differently in
128 different configurations of Mercurial, the test will fail and a minimized
129 different configurations of Mercurial, the test will fail and a minimized
129 .t test file will be written to the hypothesis-generated directory to
130 .t test file will be written to the hypothesis-generated directory to
130 exhibit that failure.
131 exhibit that failure.
131
132
132 Operations are defined as methods with @rule() decorators. See the
133 Operations are defined as methods with @rule() decorators. See the
133 Hypothesis documentation at
134 Hypothesis documentation at
134 http://hypothesis.readthedocs.org/en/release/stateful.html for more
135 http://hypothesis.readthedocs.org/en/release/stateful.html for more
135 details."""
136 details."""
136
137
137 # A bundle is a reusable collection of previously generated data which may
138 # A bundle is a reusable collection of previously generated data which may
138 # be provided as arguments to future operations.
139 # be provided as arguments to future operations.
139 repos = Bundle('repos')
140 repos = Bundle('repos')
140 paths = Bundle('paths')
141 paths = Bundle('paths')
141 contents = Bundle('contents')
142 contents = Bundle('contents')
142 branches = Bundle('branches')
143 branches = Bundle('branches')
143 committimes = Bundle('committimes')
144 committimes = Bundle('committimes')
144
145
145 def __init__(self):
146 def __init__(self):
146 super(verifyingstatemachine, self).__init__()
147 super(verifyingstatemachine, self).__init__()
147 self.repodir = os.path.join(testtmp, "repos")
148 self.repodir = os.path.join(testtmp, "repos")
148 if os.path.exists(self.repodir):
149 if os.path.exists(self.repodir):
149 shutil.rmtree(self.repodir)
150 shutil.rmtree(self.repodir)
150 os.chdir(testtmp)
151 os.chdir(testtmp)
151 self.log = []
152 self.log = []
152 self.failed = False
153 self.failed = False
153
154
154 self.mkdirp("repos")
155 self.mkdirp("repos")
155 self.cd("repos")
156 self.cd("repos")
156 self.mkdirp("repo1")
157 self.mkdirp("repo1")
157 self.cd("repo1")
158 self.cd("repo1")
158 self.hg("init")
159 self.hg("init")
159 self.extensions = {}
160 self.extensions = {}
160 self.all_extensions = set()
161 self.all_extensions = set()
161 self.non_skippable_extensions = set()
162 self.non_skippable_extensions = set()
162
163
163 def teardown(self):
164 def teardown(self):
164 """On teardown we clean up after ourselves as usual, but we also
165 """On teardown we clean up after ourselves as usual, but we also
165 do some additional testing: We generate a .t file based on our test
166 do some additional testing: We generate a .t file based on our test
166 run using run-test.py -i to get the correct output.
167 run using run-test.py -i to get the correct output.
167
168
168 We then test it in a number of other configurations, verifying that
169 We then test it in a number of other configurations, verifying that
169 each passes the same test."""
170 each passes the same test."""
170 super(verifyingstatemachine, self).teardown()
171 super(verifyingstatemachine, self).teardown()
171 try:
172 try:
172 shutil.rmtree(self.repodir)
173 shutil.rmtree(self.repodir)
173 except OSError:
174 except OSError:
174 pass
175 pass
175 ttest = os.linesep.join(" " + l for l in self.log)
176 ttest = os.linesep.join(" " + l for l in self.log)
176 os.chdir(testtmp)
177 os.chdir(testtmp)
177 path = os.path.join(testtmp, "test-generated.t")
178 path = os.path.join(testtmp, "test-generated.t")
178 with open(path, 'w') as o:
179 with open(path, 'w') as o:
179 o.write(ttest + os.linesep)
180 o.write(ttest + os.linesep)
180 with open(os.devnull, "w") as devnull:
181 with open(os.devnull, "w") as devnull:
181 rewriter = subprocess.Popen(
182 rewriter = subprocess.Popen(
182 [runtests, "--local", "-i", path], stdin=subprocess.PIPE,
183 [runtests, "--local", "-i", path], stdin=subprocess.PIPE,
183 stdout=devnull, stderr=devnull,
184 stdout=devnull, stderr=devnull,
184 )
185 )
185 rewriter.communicate("yes")
186 rewriter.communicate("yes")
186 with open(path, 'r') as i:
187 with open(path, 'r') as i:
187 ttest = i.read()
188 ttest = i.read()
188
189
189 e = None
190 e = None
190 if not self.failed:
191 if not self.failed:
191 try:
192 try:
192 for ext in (
193 for ext in (
193 self.all_extensions - self.non_skippable_extensions
194 self.all_extensions - self.non_skippable_extensions
194 ):
195 ):
195 try:
196 try:
196 os.environ["SKIP_EXTENSION"] = ext
197 os.environ["SKIP_EXTENSION"] = ext
197 output = subprocess.check_output([
198 output = subprocess.check_output([
198 runtests, path, "--local",
199 runtests, path, "--local",
199 ], stderr=subprocess.STDOUT)
200 ], stderr=subprocess.STDOUT)
200 assert "Ran 1 test" in output, output
201 assert "Ran 1 test" in output, output
201 finally:
202 finally:
202 del os.environ["SKIP_EXTENSION"]
203 del os.environ["SKIP_EXTENSION"]
203 output = subprocess.check_output([
204 output = subprocess.check_output([
204 runtests, path, "--local", "--pure"
205 runtests, path, "--local", "--pure"
205 ], stderr=subprocess.STDOUT)
206 ], stderr=subprocess.STDOUT)
206 assert "Ran 1 test" in output, output
207 assert "Ran 1 test" in output, output
207 except subprocess.CalledProcessError as e:
208 except subprocess.CalledProcessError as e:
208 note(e.output)
209 note(e.output)
209 finally:
210 finally:
210 os.unlink(path)
211 os.unlink(path)
211 try:
212 try:
212 os.unlink(path + ".err")
213 os.unlink(path + ".err")
213 except OSError:
214 except OSError:
214 pass
215 pass
215 if self.failed or e is not None:
216 if self.failed or e is not None:
216 with open(savefile, "wb") as o:
217 with open(savefile, "wb") as o:
217 o.write(ttest)
218 o.write(ttest)
218 if e is not None:
219 if e is not None:
219 raise e
220 raise e
220
221
221 def execute_step(self, step):
222 def execute_step(self, step):
222 try:
223 try:
223 return super(verifyingstatemachine, self).execute_step(step)
224 return super(verifyingstatemachine, self).execute_step(step)
224 except (HypothesisException, KeyboardInterrupt):
225 except (HypothesisException, KeyboardInterrupt):
225 raise
226 raise
226 except Exception:
227 except Exception:
227 self.failed = True
228 self.failed = True
228 raise
229 raise
229
230
230 # Section: Basic commands.
231 # Section: Basic commands.
231 def mkdirp(self, path):
232 def mkdirp(self, path):
232 if os.path.exists(path):
233 if os.path.exists(path):
233 return
234 return
234 self.log.append(
235 self.log.append(
235 "$ mkdir -p -- %s" % (pipes.quote(os.path.relpath(path)),))
236 "$ mkdir -p -- %s" % (pipes.quote(os.path.relpath(path)),))
236 os.makedirs(path)
237 os.makedirs(path)
237
238
238 def cd(self, path):
239 def cd(self, path):
239 path = os.path.relpath(path)
240 path = os.path.relpath(path)
240 if path == ".":
241 if path == ".":
241 return
242 return
242 os.chdir(path)
243 os.chdir(path)
243 self.log.append("$ cd -- %s" % (pipes.quote(path),))
244 self.log.append("$ cd -- %s" % (pipes.quote(path),))
244
245
245 def hg(self, *args):
246 def hg(self, *args):
246 self.command("hg", *args)
247 self.command("hg", *args)
247
248
248 def command(self, *args):
249 def command(self, *args):
249 self.log.append("$ " + ' '.join(map(pipes.quote, args)))
250 self.log.append("$ " + ' '.join(map(pipes.quote, args)))
250 subprocess.check_output(args, stderr=subprocess.STDOUT)
251 subprocess.check_output(args, stderr=subprocess.STDOUT)
251
252
252 # Section: Set up basic data
253 # Section: Set up basic data
253 # This section has no side effects but generates data that we will want
254 # This section has no side effects but generates data that we will want
254 # to use later.
255 # to use later.
255 @rule(
256 @rule(
256 target=paths,
257 target=paths,
257 source=st.lists(files, min_size=1).map(lambda l: os.path.join(*l)))
258 source=st.lists(files, min_size=1).map(lambda l: os.path.join(*l)))
258 def genpath(self, source):
259 def genpath(self, source):
259 return source
260 return source
260
261
261 @rule(
262 @rule(
262 target=committimes,
263 target=committimes,
263 when=datetimes(min_year=1970, max_year=2038) | st.none())
264 when=datetimes(min_year=1970, max_year=2038) | st.none())
264 def gentime(self, when):
265 def gentime(self, when):
265 return when
266 return when
266
267
267 @rule(
268 @rule(
268 target=contents,
269 target=contents,
269 content=st.one_of(
270 content=st.one_of(
270 st.binary(),
271 st.binary(),
271 st.text().map(lambda x: x.encode('utf-8'))
272 st.text().map(lambda x: x.encode('utf-8'))
272 ))
273 ))
273 def gencontent(self, content):
274 def gencontent(self, content):
274 return content
275 return content
275
276
276 @rule(
277 @rule(
277 target=branches,
278 target=branches,
278 name=safetext,
279 name=safetext,
279 )
280 )
280 def genbranch(self, name):
281 def genbranch(self, name):
281 return name
282 return name
282
283
283 @rule(target=paths, source=paths)
284 @rule(target=paths, source=paths)
284 def lowerpath(self, source):
285 def lowerpath(self, source):
285 return source.lower()
286 return source.lower()
286
287
287 @rule(target=paths, source=paths)
288 @rule(target=paths, source=paths)
288 def upperpath(self, source):
289 def upperpath(self, source):
289 return source.upper()
290 return source.upper()
290
291
291 # Section: Basic path operations
292 # Section: Basic path operations
292 @rule(path=paths, content=contents)
293 @rule(path=paths, content=contents)
293 def writecontent(self, path, content):
294 def writecontent(self, path, content):
294 self.unadded_changes = True
295 self.unadded_changes = True
295 if os.path.isdir(path):
296 if os.path.isdir(path):
296 return
297 return
297 parent = os.path.dirname(path)
298 parent = os.path.dirname(path)
298 if parent:
299 if parent:
299 try:
300 try:
300 self.mkdirp(parent)
301 self.mkdirp(parent)
301 except OSError:
302 except OSError:
302 # It may be the case that there is a regular file that has
303 # It may be the case that there is a regular file that has
303 # previously been created that has the same name as an ancestor
304 # previously been created that has the same name as an ancestor
304 # of the current path. This will cause mkdirp to fail with this
305 # of the current path. This will cause mkdirp to fail with this
305 # error. We just turn this into a no-op in that case.
306 # error. We just turn this into a no-op in that case.
306 return
307 return
307 with open(path, 'wb') as o:
308 with open(path, 'wb') as o:
308 o.write(content)
309 o.write(content)
309 self.log.append((
310 self.log.append((
310 "$ python -c 'import binascii; "
311 "$ python -c 'import binascii; "
311 "print(binascii.unhexlify(\"%s\"))' > %s") % (
312 "print(binascii.unhexlify(\"%s\"))' > %s") % (
312 binascii.hexlify(content),
313 binascii.hexlify(content),
313 pipes.quote(path),
314 pipes.quote(path),
314 ))
315 ))
315
316
316 @rule(path=paths)
317 @rule(path=paths)
317 def addpath(self, path):
318 def addpath(self, path):
318 if os.path.exists(path):
319 if os.path.exists(path):
319 self.hg("add", "--", path)
320 self.hg("add", "--", path)
320
321
321 @rule(path=paths)
322 @rule(path=paths)
322 def forgetpath(self, path):
323 def forgetpath(self, path):
323 if os.path.exists(path):
324 if os.path.exists(path):
324 with acceptableerrors(
325 with acceptableerrors(
325 "file is already untracked",
326 "file is already untracked",
326 ):
327 ):
327 self.hg("forget", "--", path)
328 self.hg("forget", "--", path)
328
329
329 @rule(s=st.none() | st.integers(0, 100))
330 @rule(s=st.none() | st.integers(0, 100))
330 def addremove(self, s):
331 def addremove(self, s):
331 args = ["addremove"]
332 args = ["addremove"]
332 if s is not None:
333 if s is not None:
333 args.extend(["-s", str(s)])
334 args.extend(["-s", str(s)])
334 self.hg(*args)
335 self.hg(*args)
335
336
336 @rule(path=paths)
337 @rule(path=paths)
337 def removepath(self, path):
338 def removepath(self, path):
338 if os.path.exists(path):
339 if os.path.exists(path):
339 with acceptableerrors(
340 with acceptableerrors(
340 'file is untracked',
341 'file is untracked',
341 'file has been marked for add',
342 'file has been marked for add',
342 'file is modified',
343 'file is modified',
343 ):
344 ):
344 self.hg("remove", "--", path)
345 self.hg("remove", "--", path)
345
346
346 @rule(
347 @rule(
347 message=safetext,
348 message=safetext,
348 amend=st.booleans(),
349 amend=st.booleans(),
349 when=committimes,
350 when=committimes,
350 addremove=st.booleans(),
351 addremove=st.booleans(),
351 secret=st.booleans(),
352 secret=st.booleans(),
352 close_branch=st.booleans(),
353 close_branch=st.booleans(),
353 )
354 )
354 def maybecommit(
355 def maybecommit(
355 self, message, amend, when, addremove, secret, close_branch
356 self, message, amend, when, addremove, secret, close_branch
356 ):
357 ):
357 command = ["commit"]
358 command = ["commit"]
358 errors = ["nothing changed"]
359 errors = ["nothing changed"]
359 if amend:
360 if amend:
360 errors.append("cannot amend public changesets")
361 errors.append("cannot amend public changesets")
361 command.append("--amend")
362 command.append("--amend")
362 command.append("-m" + pipes.quote(message))
363 command.append("-m" + pipes.quote(message))
363 if secret:
364 if secret:
364 command.append("--secret")
365 command.append("--secret")
365 if close_branch:
366 if close_branch:
366 command.append("--close-branch")
367 command.append("--close-branch")
367 errors.append("can only close branch heads")
368 errors.append("can only close branch heads")
368 if addremove:
369 if addremove:
369 command.append("--addremove")
370 command.append("--addremove")
370 if when is not None:
371 if when is not None:
371 if when.year == 1970:
372 if when.year == 1970:
372 errors.append('negative date value')
373 errors.append('negative date value')
373 if when.year == 2038:
374 if when.year == 2038:
374 errors.append('exceeds 32 bits')
375 errors.append('exceeds 32 bits')
375 command.append("--date=%s" % (
376 command.append("--date=%s" % (
376 when.strftime('%Y-%m-%d %H:%M:%S %z'),))
377 when.strftime('%Y-%m-%d %H:%M:%S %z'),))
377
378
378 with acceptableerrors(*errors):
379 with acceptableerrors(*errors):
379 self.hg(*command)
380 self.hg(*command)
380
381
381 # Section: Repository management
382 # Section: Repository management
382 @property
383 @property
383 def currentrepo(self):
384 def currentrepo(self):
384 return os.path.basename(os.getcwd())
385 return os.path.basename(os.getcwd())
385
386
386 @rule(
387 @rule(
387 target=repos,
388 target=repos,
388 source=repos,
389 source=repos,
389 name=reponames,
390 name=reponames,
390 )
391 )
391 def clone(self, source, name):
392 def clone(self, source, name):
392 if not os.path.exists(os.path.join("..", name)):
393 if not os.path.exists(os.path.join("..", name)):
393 self.cd("..")
394 self.cd("..")
394 self.hg("clone", source, name)
395 self.hg("clone", source, name)
395 self.cd(name)
396 self.cd(name)
396 return name
397 return name
397
398
398 @rule(
399 @rule(
399 target=repos,
400 target=repos,
400 name=reponames,
401 name=reponames,
401 )
402 )
402 def fresh(self, name):
403 def fresh(self, name):
403 if not os.path.exists(os.path.join("..", name)):
404 if not os.path.exists(os.path.join("..", name)):
404 self.cd("..")
405 self.cd("..")
405 self.mkdirp(name)
406 self.mkdirp(name)
406 self.cd(name)
407 self.cd(name)
407 self.hg("init")
408 self.hg("init")
408 return name
409 return name
409
410
410 @rule(name=repos)
411 @rule(name=repos)
411 def switch(self, name):
412 def switch(self, name):
412 self.cd(os.path.join("..", name))
413 self.cd(os.path.join("..", name))
413 assert self.currentrepo == name
414 assert self.currentrepo == name
414 assert os.path.exists(".hg")
415 assert os.path.exists(".hg")
415
416
416 @rule(target=repos)
417 @rule(target=repos)
417 def origin(self):
418 def origin(self):
418 return "repo1"
419 return "repo1"
419
420
420 @rule()
421 @rule()
421 def pull(self, repo=repos):
422 def pull(self, repo=repos):
422 with acceptableerrors(
423 with acceptableerrors(
423 "repository default not found",
424 "repository default not found",
424 "repository is unrelated",
425 "repository is unrelated",
425 ):
426 ):
426 self.hg("pull")
427 self.hg("pull")
427
428
428 @rule(newbranch=st.booleans())
429 @rule(newbranch=st.booleans())
429 def push(self, newbranch):
430 def push(self, newbranch):
430 with acceptableerrors(
431 with acceptableerrors(
431 "default repository not configured",
432 "default repository not configured",
432 "no changes found",
433 "no changes found",
433 ):
434 ):
434 if newbranch:
435 if newbranch:
435 self.hg("push", "--new-branch")
436 self.hg("push", "--new-branch")
436 else:
437 else:
437 with acceptableerrors(
438 with acceptableerrors(
438 "creates new branches"
439 "creates new branches"
439 ):
440 ):
440 self.hg("push")
441 self.hg("push")
441
442
442 # Section: Simple side effect free "check" operations
443 # Section: Simple side effect free "check" operations
443 @rule()
444 @rule()
444 def log(self):
445 def log(self):
445 self.hg("log")
446 self.hg("log")
446
447
447 @rule()
448 @rule()
448 def verify(self):
449 def verify(self):
449 self.hg("verify")
450 self.hg("verify")
450
451
451 @rule()
452 @rule()
452 def diff(self):
453 def diff(self):
453 self.hg("diff", "--nodates")
454 self.hg("diff", "--nodates")
454
455
455 @rule()
456 @rule()
456 def status(self):
457 def status(self):
457 self.hg("status")
458 self.hg("status")
458
459
459 @rule()
460 @rule()
460 def export(self):
461 def export(self):
461 self.hg("export")
462 self.hg("export")
462
463
463 # Section: Branch management
464 # Section: Branch management
464 @rule()
465 @rule()
465 def checkbranch(self):
466 def checkbranch(self):
466 self.hg("branch")
467 self.hg("branch")
467
468
468 @rule(branch=branches)
469 @rule(branch=branches)
469 def switchbranch(self, branch):
470 def switchbranch(self, branch):
470 with acceptableerrors(
471 with acceptableerrors(
471 'cannot use an integer as a name',
472 'cannot use an integer as a name',
472 'cannot be used in a name',
473 'cannot be used in a name',
473 'a branch of the same name already exists',
474 'a branch of the same name already exists',
474 'is reserved',
475 'is reserved',
475 ):
476 ):
476 self.hg("branch", "--", branch)
477 self.hg("branch", "--", branch)
477
478
478 @rule(branch=branches, clean=st.booleans())
479 @rule(branch=branches, clean=st.booleans())
479 def update(self, branch, clean):
480 def update(self, branch, clean):
480 with acceptableerrors(
481 with acceptableerrors(
481 'unknown revision',
482 'unknown revision',
482 'parse error',
483 'parse error',
483 ):
484 ):
484 if clean:
485 if clean:
485 self.hg("update", "-C", "--", branch)
486 self.hg("update", "-C", "--", branch)
486 else:
487 else:
487 self.hg("update", "--", branch)
488 self.hg("update", "--", branch)
488
489
489 # Section: Extension management
490 # Section: Extension management
490 def hasextension(self, extension):
491 def hasextension(self, extension):
491 repo = self.currentrepo
492 repo = self.currentrepo
492 return repo in self.extensions and extension in self.extensions[repo]
493 return repo in self.extensions and extension in self.extensions[repo]
493
494
494 def commandused(self, extension):
495 def commandused(self, extension):
495 assert extension in self.all_extensions
496 assert extension in self.all_extensions
496 self.non_skippable_extensions.add(extension)
497 self.non_skippable_extensions.add(extension)
497
498
498 @rule(extension=st.sampled_from((
499 @rule(extension=st.sampled_from((
499 'shelve', 'mq', 'blackbox',
500 'shelve', 'mq', 'blackbox',
500 )))
501 )))
501 def addextension(self, extension):
502 def addextension(self, extension):
502 self.all_extensions.add(extension)
503 self.all_extensions.add(extension)
503 extensions = self.extensions.setdefault(self.currentrepo, set())
504 extensions = self.extensions.setdefault(self.currentrepo, set())
504 if extension in extensions:
505 if extension in extensions:
505 return
506 return
506 extensions.add(extension)
507 extensions.add(extension)
507 if not os.path.exists(hgrc):
508 if not os.path.exists(hgrc):
508 self.command("touch", hgrc)
509 self.command("touch", hgrc)
509 with open(hgrc, 'a') as o:
510 with open(hgrc, 'a') as o:
510 line = "[extensions]\n%s=\n" % (extension,)
511 line = "[extensions]\n%s=\n" % (extension,)
511 o.write(line)
512 o.write(line)
512 for l in line.splitlines():
513 for l in line.splitlines():
513 self.log.append((
514 self.log.append((
514 '$ if test "$SKIP_EXTENSION" != "%s" ; '
515 '$ if test "$SKIP_EXTENSION" != "%s" ; '
515 'then echo %r >> %s; fi') % (
516 'then echo %r >> %s; fi') % (
516 extension, l, hgrc,))
517 extension, l, hgrc,))
517
518
518 # Section: Commands from the shelve extension
519 # Section: Commands from the shelve extension
519 @rule()
520 @rule()
520 @precondition(lambda self: self.hasextension("shelve"))
521 @precondition(lambda self: self.hasextension("shelve"))
521 def shelve(self):
522 def shelve(self):
522 self.commandused("shelve")
523 self.commandused("shelve")
523 with acceptableerrors("nothing changed"):
524 with acceptableerrors("nothing changed"):
524 self.hg("shelve")
525 self.hg("shelve")
525
526
526 @rule()
527 @rule()
527 @precondition(lambda self: self.hasextension("shelve"))
528 @precondition(lambda self: self.hasextension("shelve"))
528 def unshelve(self):
529 def unshelve(self):
529 self.commandused("shelve")
530 self.commandused("shelve")
530 with acceptableerrors("no shelved changes to apply"):
531 with acceptableerrors("no shelved changes to apply"):
531 self.hg("unshelve")
532 self.hg("unshelve")
532
533
534 class writeonlydatabase(ExampleDatabase):
535 def __init__(self, underlying):
536 super(ExampleDatabase, self).__init__()
537 self.underlying = underlying
538
539 def fetch(self, key):
540 return ()
541
542 def save(self, key, value):
543 self.underlying.save(key, value)
544
545 def delete(self, key, value):
546 self.underlying.delete(key, value)
547
548 def close(self):
549 self.underlying.close()
550
533 settings.register_profile(
551 settings.register_profile(
534 'default', settings(
552 'default', settings(
535 timeout=300,
553 timeout=300,
536 stateful_step_count=50,
554 stateful_step_count=50,
537 max_examples=10,
555 max_examples=10,
538 )
556 )
539 )
557 )
540
558
541 settings.register_profile(
559 settings.register_profile(
542 'fast', settings(
560 'fast', settings(
543 timeout=10,
561 timeout=10,
544 stateful_step_count=20,
562 stateful_step_count=20,
545 max_examples=5,
563 max_examples=5,
546 min_satisfying_examples=1,
564 min_satisfying_examples=1,
547 max_shrinks=0,
565 max_shrinks=0,
548 )
566 )
549 )
567 )
550
568
569 settings.register_profile(
570 'continuous', settings(
571 timeout=-1,
572 stateful_step_count=1000,
573 max_examples=10 ** 8,
574 max_iterations=10 ** 8,
575 database=writeonlydatabase(settings.default.database)
576 )
577 )
578
551 settings.load_profile(os.getenv('HYPOTHESIS_PROFILE', 'default'))
579 settings.load_profile(os.getenv('HYPOTHESIS_PROFILE', 'default'))
552
580
553 verifyingtest = verifyingstatemachine.TestCase
581 verifyingtest = verifyingstatemachine.TestCase
554
582
555 verifyingtest.settings = settings.default
583 verifyingtest.settings = settings.default
556
584
557 if __name__ == '__main__':
585 if __name__ == '__main__':
558 try:
586 try:
559 silenttestrunner.main(__name__)
587 silenttestrunner.main(__name__)
560 finally:
588 finally:
561 # So as to prevent proliferation of useless test files, if we never
589 # So as to prevent proliferation of useless test files, if we never
562 # actually wrote a failing test we clean up after ourselves and delete
590 # actually wrote a failing test we clean up after ourselves and delete
563 # the file for doing so that we owned.
591 # the file for doing so that we owned.
564 if os.path.exists(savefile) and os.path.getsize(savefile) == 0:
592 if os.path.exists(savefile) and os.path.getsize(savefile) == 0:
565 os.unlink(savefile)
593 os.unlink(savefile)
General Comments 0
You need to be logged in to leave comments. Login now