Ticket #14292: 14292_doctest_race.patch

File 14292_doctest_race.patch, 10.0 KB (added by jdemeyer, 7 years ago)
  • sage/doctest/control.py

    # HG changeset patch
    # User Jeroen Demeyer <jdemeyer@cage.ugent.be>
    # Date 1365077548 -7200
    # Node ID 2ca5f1f4712199fa977e9bd7d00a48dc4e3e9b75
    # Parent  99a6c4527bb9f7cf93eada1f63a08787d4e68cad
    Fix race conditions in doctester
    
    diff --git a/sage/doctest/control.py b/sage/doctest/control.py
    a b  
    9494        # We don't want to use the real stats file by default so that
    9595        # we don't overwrite timings for the actual running doctests.
    9696        self.stats_path = os.path.join(DOT_SAGE, "timings_dt_test.json")
    97         if not os.path.exists(self.stats_path):
    98             with open(self.stats_path, "w") as stats_file:
    99                 json.dump({},stats_file)
    10097        self.__dict__.update(kwds)
    10198
    10299    def _repr_(self):
     
    276273            sage: D['sage.doctest.control']
    277274            {u'walltime': 1.0}
    278275        """
    279         with open(filename, 'w') as stats_file:
     276        from sage.misc.temporary_file import atomic_write
     277        with atomic_write(filename) as stats_file:
    280278            json.dump(self.stats, stats_file)
    281279
     280
    282281    def log(self, s, end="\n"):
    283282        """
    284283        Logs the string ``s + end`` (where ``end`` is a newline by default)
  • sage/interfaces/gap.py

    diff --git a/sage/interfaces/gap.py b/sage/interfaces/gap.py
    a b  
    12551255        # SaveWorkspace can only be used at the main gap> prompt. It cannot
    12561256        # be included in the body of a loop or function, or called from a
    12571257        # break loop.
    1258 
    1259         # Save the worksheet to a temporary file and then move that
    1260         # file in place to avoid race conditions.
    1261         WORKSPACE_TMP = "%s-%s"%(WORKSPACE, self.pid())
    1262         self.eval('SaveWorkspace("%s");'%WORKSPACE_TMP, allow_use_file=False)
    1263         try:
    1264             os.rename(WORKSPACE_TMP, WORKSPACE)
    1265         except OSError:
    1266             # Some operating systems might not support in-place
    1267             # renaming. We delete the original file first.
    1268             os.unlink(WORKSPACE)
    1269             os.rename(WORKSPACE_TMP, WORKSPACE)
     1258        from sage.misc.temporary_file import atomic_write
     1259        with atomic_write(WORKSPACE) as f:
     1260            f.close()
     1261            self.eval('SaveWorkspace("%s");'%(f.name), allow_use_file=False)
    12701262
    12711263    # Todo -- this -- but there is a tricky "when does it end" issue!
    12721264    # Maybe do via a file somehow?
  • sage/misc/temporary_file.py

    diff --git a/sage/misc/temporary_file.py b/sage/misc/temporary_file.py
    a b  
    55
    66- Volker Braun, Jeroen Demeyer (2012-10-18): move these functions here
    77  from sage/misc/misc.py and make them secure, see :trac:`13579`.
     8
     9- Jeroen Demeyer (2013-03-17): add class:`atomic_write`,
     10  see :trac:`14292`.
    811"""
    912
    1013#*****************************************************************************
     
    3538        sage: child_SAGE_TMP, err, ret = test_executable(["sage", "-c", "print SAGE_TMP"])
    3639        sage: err, ret
    3740        ('', 0)
    38         sage: os.path.exists(child_SAGE_TMP)
     41        sage: os.path.exists(child_SAGE_TMP)  # indirect doctest
    3942        False
    4043
    4144    The parent directory should exist::
     
    152155        i += 1
    153156    filename = 'sage%d.%s'%(i,ext)
    154157    return filename
     158
     159#################################################################
     160# write to a temporary file and move it in place
     161#################################################################
     162class atomic_write:
     163    """
     164    Write to a given file using a temporary file and then rename it
     165    to the target file. This renaming should be atomic on modern
     166    operating systems. Therefore, this class can be used to avoid race
     167    conditions when a file might be read while it is being written.
     168    It also avoids having partially written files due to exceptions
     169    or crashes.
     170
     171    This is to be used in a ``with`` statement, where a temporary file
     172    is created when entering the ``with`` and is moved in place of the
     173    target file when exiting the ``with`` (if no exceptions occured).
     174
     175    INPUT:
     176
     177    - ``target_filename`` -- the name of the file to be written.
     178      Normally, the contents of this file will be overwritten.
     179
     180    - ``append`` -- (boolean, default: False) if True and
     181      ``target_filename`` is an existing file, then copy the current
     182      contents of ``target_filename`` to the temporary file when
     183      entering the ``with`` statement. Otherwise, the temporary file is
     184      initially empty.
     185
     186    EXAMPLES::
     187
     188        sage: from sage.misc.temporary_file import atomic_write
     189        sage: target_file = tmp_filename()
     190        sage: open(target_file, "w").write("Old contents")
     191        sage: with atomic_write(target_file) as f:
     192        ....:     f.write("New contents")
     193        ....:     f.flush()
     194        ....:     open(target_file, "r").read()
     195        'Old contents'
     196        sage: open(target_file, "r").read()
     197        'New contents'
     198
     199    The name of the temporary file can be accessed using ``f.name``.
     200    It is not a problem to close and re-open the temporary file::
     201
     202        sage: from sage.misc.temporary_file import atomic_write
     203        sage: target_file = tmp_filename()
     204        sage: open(target_file, "w").write("Old contents")
     205        sage: with atomic_write(target_file) as f:
     206        ....:     f.close()
     207        ....:     open(f.name, "w").write("Newer contents")
     208        sage: open(target_file, "r").read()
     209        'Newer contents'
     210
     211    If an exception occurs while writing the file, the target file is
     212    not touched::
     213
     214        sage: with atomic_write(target_file) as f:
     215        ....:     f.write("Newest contents")
     216        ....:     raise RuntimeError
     217        Traceback (most recent call last):
     218        ...
     219        RuntimeError
     220        sage: open(target_file, "r").read()
     221        'Newer contents'
     222
     223    Some examples of using the ``append`` option. Note that the file
     224    is never opened in "append" mode, it is possible to overwrite
     225    existing data::
     226
     227        sage: target_file = tmp_filename()
     228        sage: with atomic_write(target_file, append=True) as f:
     229        ....:     f.write("Hello")
     230        sage: with atomic_write(target_file, append=True) as f:
     231        ....:     f.write(" World")
     232        sage: open(target_file, "r").read()
     233        'Hello World'
     234        sage: with atomic_write(target_file, append=True) as f:
     235        ....:     f.seek(0)
     236        ....:     f.write("HELLO")
     237        sage: open(target_file, "r").read()
     238        'HELLO World'
     239
     240    If the target file is a symbolic link, the link is kept and the
     241    target of the link is written to::
     242
     243        sage: link_to_target = os.path.join(tmp_dir(), "templink")
     244        sage: os.symlink(target_file, link_to_target)
     245        sage: with atomic_write(link_to_target) as f:
     246        ....:     f.write("Newest contents")
     247        sage: open(target_file, "r").read()
     248        'Newest contents'
     249
     250    Test writing twice to the same target file. The outermost ``with``
     251    "wins"::
     252
     253        sage: open(target_file, "w").write(">>> ")
     254        sage: with atomic_write(target_file, append=True) as f, \
     255        ....:          atomic_write(target_file, append=True) as g:
     256        ....:     f.write("AAA"); f.close()
     257        ....:     g.write("BBB"); g.close()
     258        sage: open(target_file, "r").read()
     259        '>>> AAA'
     260    """
     261    def __init__(self, target_filename, append=False):
     262        """
     263        TESTS::
     264
     265            sage: from sage.misc.temporary_file import atomic_write
     266            sage: link_to_target = os.path.join(tmp_dir(), "templink")
     267            sage: os.symlink("/foobar", link_to_target)
     268            sage: wvt = atomic_write(link_to_target)
     269            sage: print wvt.target
     270            /foobar
     271            sage: print wvt.tmpdir
     272            /
     273        """
     274        self.target = os.path.realpath(target_filename)
     275        self.tmpdir = os.path.dirname(self.target)
     276        self.append = append
     277
     278    def __enter__(self):
     279        """
     280        Create and return a temporary file in ``self.tmpdir`` (normally
     281        the same directory as the target file).
     282
     283        If ``self.append``, then copy the current contents of
     284        ``self.target`` to the temporary file.
     285
     286        OUTPUT: a file returned by :func:`tempfile.NamedTemporaryFile`.
     287
     288        TESTS::
     289
     290            sage: from sage.misc.temporary_file import atomic_write
     291            sage: wvt = atomic_write(tmp_filename())
     292            sage: with wvt as f:
     293            ....:     os.path.dirname(wvt.target) == os.path.dirname(f.name)
     294            True
     295        """
     296        self.tempfile = tempfile.NamedTemporaryFile(dir=self.tmpdir, delete=False)
     297        self.tempname = self.tempfile.name
     298        if self.append:
     299            try:
     300                r = open(self.target).read()
     301            except IOError:
     302                pass
     303            else:
     304                self.tempfile.write(r)
     305        return self.tempfile
     306
     307    def __exit__(self, exc_type, exc_val, exc_tb):
     308        """
     309        If the ``with`` block was successful, move the temporary file
     310        to the target file. Otherwise, delete the temporary file.
     311
     312        TESTS:
     313
     314        Check that the temporary file is deleted if there was an
     315        exception::
     316
     317            sage: from sage.misc.temporary_file import atomic_write
     318            sage: with atomic_write(tmp_filename()) as f:
     319            ....:     tempname = f.name
     320            ....:     raise RuntimeError
     321            Traceback (most recent call last):
     322            ...
     323            RuntimeError
     324            sage: os.path.exists(tempname)
     325            False
     326        """
     327        # Close the file (Python allows closing a closed file, so it's
     328        # okay if the user already closed it).
     329        self.tempfile.close()
     330
     331        if exc_type is None:
     332            # Success: move temporary file to target file
     333            try:
     334                os.rename(self.tempname, self.target)
     335            except OSError:
     336                os.unlink(self.target)
     337                os.rename(self.tempname, self.target)
     338        else:
     339            # Failure: delete temporary file
     340            os.unlink(self.tempname)