tests/test-verify-repo-operations.py
changeset 43076 2372284d9457
parent 28499 8b90367c4cf3
child 45942 89a2afe31e82
equal deleted inserted replaced
43075:57875cf423c9 43076:2372284d9457
     9 import os
     9 import os
    10 import subprocess
    10 import subprocess
    11 import sys
    11 import sys
    12 
    12 
    13 # Only run if slow tests are allowed
    13 # Only run if slow tests are allowed
    14 if subprocess.call(['python', '%s/hghave' % os.environ['TESTDIR'],
    14 if subprocess.call(['python', '%s/hghave' % os.environ['TESTDIR'], 'slow']):
    15                     'slow']):
       
    16     sys.exit(80)
    15     sys.exit(80)
    17 
    16 
    18 # These tests require Hypothesis and pytz to be installed.
    17 # These tests require Hypothesis and pytz to be installed.
    19 # Running 'pip install hypothesis pytz' will achieve that.
    18 # Running 'pip install hypothesis pytz' will achieve that.
    20 # Note: This won't work if you're running Python < 2.7.
    19 # Note: This won't work if you're running Python < 2.7.
    27 # If you are running an old version of pip you may find that the enum34
    26 # If you are running an old version of pip you may find that the enum34
    28 # backport is not installed automatically. If so 'pip install enum34' will
    27 # backport is not installed automatically. If so 'pip install enum34' will
    29 # fix this problem.
    28 # fix this problem.
    30 try:
    29 try:
    31     import enum
    30     import enum
       
    31 
    32     assert enum  # Silence pyflakes
    32     assert enum  # Silence pyflakes
    33 except ImportError:
    33 except ImportError:
    34     sys.stderr.write("skipped: enum34 not installed" + os.linesep)
    34     sys.stderr.write("skipped: enum34 not installed" + os.linesep)
    35     sys.exit(80)
    35     sys.exit(80)
    36 
    36 
    42 import silenttestrunner
    42 import silenttestrunner
    43 import subprocess
    43 import subprocess
    44 
    44 
    45 from hypothesis.errors import HypothesisException
    45 from hypothesis.errors import HypothesisException
    46 from hypothesis.stateful import (
    46 from hypothesis.stateful import (
    47     rule, RuleBasedStateMachine, Bundle, precondition)
    47     rule,
       
    48     RuleBasedStateMachine,
       
    49     Bundle,
       
    50     precondition,
       
    51 )
    48 from hypothesis import settings, note, strategies as st
    52 from hypothesis import settings, note, strategies as st
    49 from hypothesis.configuration import set_hypothesis_home_dir
    53 from hypothesis.configuration import set_hypothesis_home_dir
    50 from hypothesis.database import ExampleDatabase
    54 from hypothesis.database import ExampleDatabase
    51 
    55 
    52 testdir = os.path.abspath(os.environ["TESTDIR"])
    56 testdir = os.path.abspath(os.environ["TESTDIR"])
    74 # parallel we use atomic file create to ensure that we always get a unique
    78 # parallel we use atomic file create to ensure that we always get a unique
    75 # name.
    79 # name.
    76 file_index = 0
    80 file_index = 0
    77 while True:
    81 while True:
    78     file_index += 1
    82     file_index += 1
    79     savefile = os.path.join(generatedtests, "test-generated-%d.t" % (
    83     savefile = os.path.join(
    80         file_index,
    84         generatedtests, "test-generated-%d.t" % (file_index,)
    81     ))
    85     )
    82     try:
    86     try:
    83         os.close(os.open(savefile, os.O_CREAT | os.O_EXCL | os.O_WRONLY))
    87         os.close(os.open(savefile, os.O_CREAT | os.O_EXCL | os.O_WRONLY))
    84         break
    88         break
    85     except OSError as e:
    89     except OSError as e:
    86         if e.errno != errno.EEXIST:
    90         if e.errno != errno.EEXIST:
    92 filecharacters = (
    96 filecharacters = (
    93     "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    97     "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    94     "[]^_`;=@{}~ !#$%&'()+,-"
    98     "[]^_`;=@{}~ !#$%&'()+,-"
    95 )
    99 )
    96 
   100 
    97 files = st.text(filecharacters, min_size=1).map(lambda x: x.strip()).filter(
   101 files = (
    98     bool).map(lambda s: s.encode('ascii'))
   102     st.text(filecharacters, min_size=1)
    99 
   103     .map(lambda x: x.strip())
   100 safetext = st.text(st.characters(
   104     .filter(bool)
   101     min_codepoint=1, max_codepoint=127,
   105     .map(lambda s: s.encode('ascii'))
   102     blacklist_categories=('Cc', 'Cs')), min_size=1).map(
       
   103     lambda s: s.encode('utf-8')
       
   104 )
   106 )
   105 
   107 
       
   108 safetext = st.text(
       
   109     st.characters(
       
   110         min_codepoint=1, max_codepoint=127, blacklist_categories=('Cc', 'Cs')
       
   111     ),
       
   112     min_size=1,
       
   113 ).map(lambda s: s.encode('utf-8'))
       
   114 
   106 extensions = st.sampled_from(('shelve', 'mq', 'blackbox',))
   115 extensions = st.sampled_from(('shelve', 'mq', 'blackbox',))
       
   116 
   107 
   117 
   108 @contextmanager
   118 @contextmanager
   109 def acceptableerrors(*args):
   119 def acceptableerrors(*args):
   110     """Sometimes we know an operation we're about to perform might fail, and
   120     """Sometimes we know an operation we're about to perform might fail, and
   111     we're OK with some of the failures. In those cases this may be used as a
   121     we're OK with some of the failures. In those cases this may be used as a
   116     except subprocess.CalledProcessError as e:
   126     except subprocess.CalledProcessError as e:
   117         if not any(a in e.output for a in args):
   127         if not any(a in e.output for a in args):
   118             note(e.output)
   128             note(e.output)
   119             raise
   129             raise
   120 
   130 
       
   131 
   121 reponames = st.text("abcdefghijklmnopqrstuvwxyz01234556789", min_size=1).map(
   132 reponames = st.text("abcdefghijklmnopqrstuvwxyz01234556789", min_size=1).map(
   122     lambda s: s.encode('ascii')
   133     lambda s: s.encode('ascii')
   123 )
   134 )
       
   135 
   124 
   136 
   125 class verifyingstatemachine(RuleBasedStateMachine):
   137 class verifyingstatemachine(RuleBasedStateMachine):
   126     """This defines the set of acceptable operations on a Mercurial repository
   138     """This defines the set of acceptable operations on a Mercurial repository
   127     using Hypothesis's RuleBasedStateMachine.
   139     using Hypothesis's RuleBasedStateMachine.
   128 
   140 
   186         path = os.path.join(testtmp, "test-generated.t")
   198         path = os.path.join(testtmp, "test-generated.t")
   187         with open(path, 'w') as o:
   199         with open(path, 'w') as o:
   188             o.write(ttest + os.linesep)
   200             o.write(ttest + os.linesep)
   189         with open(os.devnull, "w") as devnull:
   201         with open(os.devnull, "w") as devnull:
   190             rewriter = subprocess.Popen(
   202             rewriter = subprocess.Popen(
   191                 [runtests, "--local", "-i", path], stdin=subprocess.PIPE,
   203                 [runtests, "--local", "-i", path],
   192                 stdout=devnull, stderr=devnull,
   204                 stdin=subprocess.PIPE,
       
   205                 stdout=devnull,
       
   206                 stderr=devnull,
   193             )
   207             )
   194             rewriter.communicate("yes")
   208             rewriter.communicate("yes")
   195             with open(path, 'r') as i:
   209             with open(path, 'r') as i:
   196                 ttest = i.read()
   210                 ttest = i.read()
   197 
   211 
   198         e = None
   212         e = None
   199         if not self.failed:
   213         if not self.failed:
   200             try:
   214             try:
   201                 output = subprocess.check_output([
   215                 output = subprocess.check_output(
   202                     runtests, path, "--local", "--pure"
   216                     [runtests, path, "--local", "--pure"],
   203                 ], stderr=subprocess.STDOUT)
   217                     stderr=subprocess.STDOUT,
       
   218                 )
   204                 assert "Ran 1 test" in output, output
   219                 assert "Ran 1 test" in output, output
   205                 for ext in (
   220                 for ext in self.all_extensions - self.non_skippable_extensions:
   206                     self.all_extensions - self.non_skippable_extensions
   221                     tf = os.path.join(
   207                 ):
   222                         testtmp, "test-generated-no-%s.t" % (ext,)
   208                     tf = os.path.join(testtmp, "test-generated-no-%s.t" % (
   223                     )
   209                         ext,
       
   210                     ))
       
   211                     with open(tf, 'w') as o:
   224                     with open(tf, 'w') as o:
   212                         for l in ttest.splitlines():
   225                         for l in ttest.splitlines():
   213                             if l.startswith("  $ hg"):
   226                             if l.startswith("  $ hg"):
   214                                 l = l.replace(
   227                                 l = l.replace(
   215                                     "--config %s=" % (
   228                                     "--config %s=" % (extensionconfigkey(ext),),
   216                                         extensionconfigkey(ext),), "")
   229                                     "",
       
   230                                 )
   217                             o.write(l + os.linesep)
   231                             o.write(l + os.linesep)
   218                     with open(tf, 'r') as r:
   232                     with open(tf, 'r') as r:
   219                         t = r.read()
   233                         t = r.read()
   220                         assert ext not in t, t
   234                         assert ext not in t, t
   221                     output = subprocess.check_output([
   235                     output = subprocess.check_output(
   222                         runtests, tf, "--local",
   236                         [runtests, tf, "--local",], stderr=subprocess.STDOUT
   223                     ], stderr=subprocess.STDOUT)
   237                     )
   224                     assert "Ran 1 test" in output, output
   238                     assert "Ran 1 test" in output, output
   225             except subprocess.CalledProcessError as e:
   239             except subprocess.CalledProcessError as e:
   226                 note(e.output)
   240                 note(e.output)
   227         if self.failed or e is not None:
   241         if self.failed or e is not None:
   228             with open(savefile, "wb") as o:
   242             with open(savefile, "wb") as o:
   242     # Section: Basic commands.
   256     # Section: Basic commands.
   243     def mkdirp(self, path):
   257     def mkdirp(self, path):
   244         if os.path.exists(path):
   258         if os.path.exists(path):
   245             return
   259             return
   246         self.log.append(
   260         self.log.append(
   247             "$ mkdir -p -- %s" % (pipes.quote(os.path.relpath(path)),))
   261             "$ mkdir -p -- %s" % (pipes.quote(os.path.relpath(path)),)
       
   262         )
   248         os.makedirs(path)
   263         os.makedirs(path)
   249 
   264 
   250     def cd(self, path):
   265     def cd(self, path):
   251         path = os.path.relpath(path)
   266         path = os.path.relpath(path)
   252         if path == ".":
   267         if path == ".":
   268     # Section: Set up basic data
   283     # Section: Set up basic data
   269     # This section has no side effects but generates data that we will want
   284     # This section has no side effects but generates data that we will want
   270     # to use later.
   285     # to use later.
   271     @rule(
   286     @rule(
   272         target=paths,
   287         target=paths,
   273         source=st.lists(files, min_size=1).map(lambda l: os.path.join(*l)))
   288         source=st.lists(files, min_size=1).map(lambda l: os.path.join(*l)),
       
   289     )
   274     def genpath(self, source):
   290     def genpath(self, source):
   275         return source
   291         return source
   276 
   292 
   277     @rule(
   293     @rule(
   278         target=committimes,
   294         target=committimes,
   279         when=datetimes(min_year=1970, max_year=2038) | st.none())
   295         when=datetimes(min_year=1970, max_year=2038) | st.none(),
       
   296     )
   280     def gentime(self, when):
   297     def gentime(self, when):
   281         return when
   298         return when
   282 
   299 
   283     @rule(
   300     @rule(
   284         target=contents,
   301         target=contents,
   285         content=st.one_of(
   302         content=st.one_of(
   286             st.binary(),
   303             st.binary(), st.text().map(lambda x: x.encode('utf-8'))
   287             st.text().map(lambda x: x.encode('utf-8'))
   304         ),
   288         ))
   305     )
   289     def gencontent(self, content):
   306     def gencontent(self, content):
   290         return content
   307         return content
   291 
   308 
   292     @rule(
   309     @rule(
   293         target=branches,
   310         target=branches, name=safetext,
   294         name=safetext,
       
   295     )
   311     )
   296     def genbranch(self, name):
   312     def genbranch(self, name):
   297         return name
   313         return name
   298 
   314 
   299     @rule(target=paths, source=paths)
   315     @rule(target=paths, source=paths)
   320                 # of the current path. This will cause mkdirp to fail with this
   336                 # of the current path. This will cause mkdirp to fail with this
   321                 # error. We just turn this into a no-op in that case.
   337                 # error. We just turn this into a no-op in that case.
   322                 return
   338                 return
   323         with open(path, 'wb') as o:
   339         with open(path, 'wb') as o:
   324             o.write(content)
   340             o.write(content)
   325         self.log.append((
   341         self.log.append(
   326             "$ python -c 'import binascii; "
   342             (
   327             "print(binascii.unhexlify(\"%s\"))' > %s") % (
   343                 "$ python -c 'import binascii; "
   328                 binascii.hexlify(content),
   344                 "print(binascii.unhexlify(\"%s\"))' > %s"
   329                 pipes.quote(path),
   345             )
   330             ))
   346             % (binascii.hexlify(content), pipes.quote(path),)
       
   347         )
   331 
   348 
   332     @rule(path=paths)
   349     @rule(path=paths)
   333     def addpath(self, path):
   350     def addpath(self, path):
   334         if os.path.exists(path):
   351         if os.path.exists(path):
   335             self.hg("add", "--", path)
   352             self.hg("add", "--", path)
   336 
   353 
   337     @rule(path=paths)
   354     @rule(path=paths)
   338     def forgetpath(self, path):
   355     def forgetpath(self, path):
   339         if os.path.exists(path):
   356         if os.path.exists(path):
   340             with acceptableerrors(
   357             with acceptableerrors("file is already untracked",):
   341                 "file is already untracked",
       
   342             ):
       
   343                 self.hg("forget", "--", path)
   358                 self.hg("forget", "--", path)
   344 
   359 
   345     @rule(s=st.none() | st.integers(0, 100))
   360     @rule(s=st.none() | st.integers(0, 100))
   346     def addremove(self, s):
   361     def addremove(self, s):
   347         args = ["addremove"]
   362         args = ["addremove"]
   386         if when is not None:
   401         if when is not None:
   387             if when.year == 1970:
   402             if when.year == 1970:
   388                 errors.append('negative date value')
   403                 errors.append('negative date value')
   389             if when.year == 2038:
   404             if when.year == 2038:
   390                 errors.append('exceeds 32 bits')
   405                 errors.append('exceeds 32 bits')
   391             command.append("--date=%s" % (
   406             command.append(
   392                 when.strftime('%Y-%m-%d %H:%M:%S %z'),))
   407                 "--date=%s" % (when.strftime('%Y-%m-%d %H:%M:%S %z'),)
       
   408             )
   393 
   409 
   394         with acceptableerrors(*errors):
   410         with acceptableerrors(*errors):
   395             self.hg(*command)
   411             self.hg(*command)
   396 
   412 
   397     # Section: Repository management
   413     # Section: Repository management
   402     @property
   418     @property
   403     def config(self):
   419     def config(self):
   404         return self.configperrepo.setdefault(self.currentrepo, {})
   420         return self.configperrepo.setdefault(self.currentrepo, {})
   405 
   421 
   406     @rule(
   422     @rule(
   407         target=repos,
   423         target=repos, source=repos, name=reponames,
   408         source=repos,
       
   409         name=reponames,
       
   410     )
   424     )
   411     def clone(self, source, name):
   425     def clone(self, source, name):
   412         if not os.path.exists(os.path.join("..", name)):
   426         if not os.path.exists(os.path.join("..", name)):
   413             self.cd("..")
   427             self.cd("..")
   414             self.hg("clone", source, name)
   428             self.hg("clone", source, name)
   415             self.cd(name)
   429             self.cd(name)
   416         return name
   430         return name
   417 
   431 
   418     @rule(
   432     @rule(
   419         target=repos,
   433         target=repos, name=reponames,
   420         name=reponames,
       
   421     )
   434     )
   422     def fresh(self, name):
   435     def fresh(self, name):
   423         if not os.path.exists(os.path.join("..", name)):
   436         if not os.path.exists(os.path.join("..", name)):
   424             self.cd("..")
   437             self.cd("..")
   425             self.mkdirp(name)
   438             self.mkdirp(name)
   438         return "repo1"
   451         return "repo1"
   439 
   452 
   440     @rule()
   453     @rule()
   441     def pull(self, repo=repos):
   454     def pull(self, repo=repos):
   442         with acceptableerrors(
   455         with acceptableerrors(
   443             "repository default not found",
   456             "repository default not found", "repository is unrelated",
   444             "repository is unrelated",
       
   445         ):
   457         ):
   446             self.hg("pull")
   458             self.hg("pull")
   447 
   459 
   448     @rule(newbranch=st.booleans())
   460     @rule(newbranch=st.booleans())
   449     def push(self, newbranch):
   461     def push(self, newbranch):
   450         with acceptableerrors(
   462         with acceptableerrors(
   451             "default repository not configured",
   463             "default repository not configured", "no changes found",
   452             "no changes found",
       
   453         ):
   464         ):
   454             if newbranch:
   465             if newbranch:
   455                 self.hg("push", "--new-branch")
   466                 self.hg("push", "--new-branch")
   456             else:
   467             else:
   457                 with acceptableerrors(
   468                 with acceptableerrors("creates new branches"):
   458                     "creates new branches"
       
   459                 ):
       
   460                     self.hg("push")
   469                     self.hg("push")
   461 
   470 
   462     # Section: Simple side effect free "check" operations
   471     # Section: Simple side effect free "check" operations
   463     @rule()
   472     @rule()
   464     def log(self):
   473     def log(self):
   496             self.hg("branch", "--", branch)
   505             self.hg("branch", "--", branch)
   497 
   506 
   498     @rule(branch=branches, clean=st.booleans())
   507     @rule(branch=branches, clean=st.booleans())
   499     def update(self, branch, clean):
   508     def update(self, branch, clean):
   500         with acceptableerrors(
   509         with acceptableerrors(
   501             'unknown revision',
   510             'unknown revision', 'parse error',
   502             'parse error',
       
   503         ):
   511         ):
   504             if clean:
   512             if clean:
   505                 self.hg("update", "-C", "--", branch)
   513                 self.hg("update", "-C", "--", branch)
   506             else:
   514             else:
   507                 self.hg("update", "--", branch)
   515                 self.hg("update", "--", branch)
   536     def unshelve(self):
   544     def unshelve(self):
   537         self.commandused("shelve")
   545         self.commandused("shelve")
   538         with acceptableerrors("no shelved changes to apply"):
   546         with acceptableerrors("no shelved changes to apply"):
   539             self.hg("unshelve")
   547             self.hg("unshelve")
   540 
   548 
       
   549 
   541 class writeonlydatabase(ExampleDatabase):
   550 class writeonlydatabase(ExampleDatabase):
   542     def __init__(self, underlying):
   551     def __init__(self, underlying):
   543         super(ExampleDatabase, self).__init__()
   552         super(ExampleDatabase, self).__init__()
   544         self.underlying = underlying
   553         self.underlying = underlying
   545 
   554 
   553         self.underlying.delete(key, value)
   562         self.underlying.delete(key, value)
   554 
   563 
   555     def close(self):
   564     def close(self):
   556         self.underlying.close()
   565         self.underlying.close()
   557 
   566 
       
   567 
   558 def extensionconfigkey(extension):
   568 def extensionconfigkey(extension):
   559     return "extensions." + extension
   569     return "extensions." + extension
   560 
   570 
       
   571 
   561 settings.register_profile(
   572 settings.register_profile(
   562     'default',  settings(
   573     'default', settings(timeout=300, stateful_step_count=50, max_examples=10,)
   563         timeout=300,
       
   564         stateful_step_count=50,
       
   565         max_examples=10,
       
   566     )
       
   567 )
   574 )
   568 
   575 
   569 settings.register_profile(
   576 settings.register_profile(
   570     'fast',  settings(
   577     'fast',
       
   578     settings(
   571         timeout=10,
   579         timeout=10,
   572         stateful_step_count=20,
   580         stateful_step_count=20,
   573         max_examples=5,
   581         max_examples=5,
   574         min_satisfying_examples=1,
   582         min_satisfying_examples=1,
   575         max_shrinks=0,
   583         max_shrinks=0,
   576     )
   584     ),
   577 )
   585 )
   578 
   586 
   579 settings.register_profile(
   587 settings.register_profile(
   580     'continuous', settings(
   588     'continuous',
       
   589     settings(
   581         timeout=-1,
   590         timeout=-1,
   582         stateful_step_count=1000,
   591         stateful_step_count=1000,
   583         max_examples=10 ** 8,
   592         max_examples=10 ** 8,
   584         max_iterations=10 ** 8,
   593         max_iterations=10 ** 8,
   585         database=writeonlydatabase(settings.default.database)
   594         database=writeonlydatabase(settings.default.database),
   586     )
   595     ),
   587 )
   596 )
   588 
   597 
   589 settings.load_profile(os.getenv('HYPOTHESIS_PROFILE', 'default'))
   598 settings.load_profile(os.getenv('HYPOTHESIS_PROFILE', 'default'))
   590 
   599 
   591 verifyingtest = verifyingstatemachine.TestCase
   600 verifyingtest = verifyingstatemachine.TestCase