]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
[3.11] gh-88352: Fix logging.TimedRotatingFileHandler (GH-116191) (GH-116209)
authorMiss Islington (bot) <31488909+miss-islington@users.noreply.github.com>
Fri, 1 Mar 2024 16:41:58 +0000 (17:41 +0100)
committerGitHub <noreply@github.com>
Fri, 1 Mar 2024 16:41:58 +0000 (18:41 +0200)
* Do not overwrite already rolled over files. It happened at midnight or
  during the DST change and caused the loss of data.
* computeRollover() now always return the timestamp larger than the
  specified time.
* Fix computation of the rollover time during the DST change.
(cherry picked from commit fee86fd9a422612b39e5aabf2571b8fe4abac770)

Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
Lib/logging/handlers.py
Lib/test/test_logging.py
Misc/NEWS.d/next/Library/2024-03-01-11-57-32.gh-issue-88352.bZ68rw.rst [new file with mode: 0644]

index 9d753a565ac50bbc321a5d382d2f3a2212f9530d..1a1337729e0c4c5ff7f89e6c175916f840e2584f 100644 (file)
@@ -299,7 +299,7 @@ class TimedRotatingFileHandler(BaseRotatingHandler):
 
             r = rotate_ts - ((currentHour * 60 + currentMinute) * 60 +
                 currentSecond)
-            if r < 0:
+            if r <= 0:
                 # Rotate time is before the current time (for example when
                 # self.rotateAt is 13:45 and it now 14:15), rotation is
                 # tomorrow.
@@ -328,17 +328,18 @@ class TimedRotatingFileHandler(BaseRotatingHandler):
                         daysToWait = self.dayOfWeek - day
                     else:
                         daysToWait = 6 - day + self.dayOfWeek + 1
-                    newRolloverAt = result + (daysToWait * (60 * 60 * 24))
-                    if not self.utc:
-                        dstNow = t[-1]
-                        dstAtRollover = time.localtime(newRolloverAt)[-1]
-                        if dstNow != dstAtRollover:
-                            if not dstNow:  # DST kicks in before next rollover, so we need to deduct an hour
-                                addend = -3600
-                            else:           # DST bows out before next rollover, so we need to add an hour
-                                addend = 3600
-                            newRolloverAt += addend
-                    result = newRolloverAt
+                    result += daysToWait * (60 * 60 * 24)
+            if not self.utc:
+                dstNow = t[-1]
+                dstAtRollover = time.localtime(result)[-1]
+                if dstNow != dstAtRollover:
+                    if not dstNow:  # DST kicks in before next rollover, so we need to deduct an hour
+                        addend = -3600
+                        if not time.localtime(result-3600)[-1]:
+                            addend = 0
+                    else:           # DST bows out before next rollover, so we need to add an hour
+                        addend = 3600
+                    result += addend
         return result
 
     def shouldRollover(self, record):
@@ -415,17 +416,14 @@ class TimedRotatingFileHandler(BaseRotatingHandler):
         then we have to get a list of matching filenames, sort them and remove
         the one with the oldest suffix.
         """
-        if self.stream:
-            self.stream.close()
-            self.stream = None
         # get the time that this sequence started at and make it a TimeTuple
         currentTime = int(time.time())
-        dstNow = time.localtime(currentTime)[-1]
         t = self.rolloverAt - self.interval
         if self.utc:
             timeTuple = time.gmtime(t)
         else:
             timeTuple = time.localtime(t)
+            dstNow = time.localtime(currentTime)[-1]
             dstThen = timeTuple[-1]
             if dstNow != dstThen:
                 if dstNow:
@@ -436,26 +434,19 @@ class TimedRotatingFileHandler(BaseRotatingHandler):
         dfn = self.rotation_filename(self.baseFilename + "." +
                                      time.strftime(self.suffix, timeTuple))
         if os.path.exists(dfn):
-            os.remove(dfn)
+            # Already rolled over.
+            return
+
+        if self.stream:
+            self.stream.close()
+            self.stream = None
         self.rotate(self.baseFilename, dfn)
         if self.backupCount > 0:
             for s in self.getFilesToDelete():
                 os.remove(s)
         if not self.delay:
             self.stream = self._open()
-        newRolloverAt = self.computeRollover(currentTime)
-        while newRolloverAt <= currentTime:
-            newRolloverAt = newRolloverAt + self.interval
-        #If DST changes and midnight or weekly rollover, adjust for this.
-        if (self.when == 'MIDNIGHT' or self.when.startswith('W')) and not self.utc:
-            dstAtRollover = time.localtime(newRolloverAt)[-1]
-            if dstNow != dstAtRollover:
-                if not dstNow:  # DST kicks in before next rollover, so we need to deduct an hour
-                    addend = -3600
-                else:           # DST bows out before next rollover, so we need to add an hour
-                    addend = 3600
-                newRolloverAt += addend
-        self.rolloverAt = newRolloverAt
+        self.rolloverAt = self.computeRollover(currentTime)
 
 class WatchedFileHandler(logging.FileHandler):
     """
index 5da4fa1ab1936e46859a5f0b76942075369d1953..e3e9f08a089aae08cd9c8bfa194e992e1cf21947 100644 (file)
@@ -5654,6 +5654,73 @@ class TimedRotatingFileHandlerTest(BaseFileTest):
                     print(tf.read())
         self.assertTrue(found, msg=msg)
 
+    def test_rollover_at_midnight(self):
+        atTime = datetime.datetime.now().time()
+        fmt = logging.Formatter('%(asctime)s %(message)s')
+        for i in range(3):
+            fh = logging.handlers.TimedRotatingFileHandler(
+                self.fn, encoding="utf-8", when='MIDNIGHT', atTime=atTime)
+            fh.setFormatter(fmt)
+            r2 = logging.makeLogRecord({'msg': f'testing1 {i}'})
+            fh.emit(r2)
+            fh.close()
+        self.assertLogFile(self.fn)
+        with open(self.fn, encoding="utf-8") as f:
+            for i, line in enumerate(f):
+                self.assertIn(f'testing1 {i}', line)
+
+        os.utime(self.fn, (time.time() - 1,)*2)
+        for i in range(2):
+            fh = logging.handlers.TimedRotatingFileHandler(
+                self.fn, encoding="utf-8", when='MIDNIGHT', atTime=atTime)
+            fh.setFormatter(fmt)
+            r2 = logging.makeLogRecord({'msg': f'testing2 {i}'})
+            fh.emit(r2)
+            fh.close()
+        rolloverDate = datetime.datetime.now() - datetime.timedelta(days=1)
+        otherfn = f'{self.fn}.{rolloverDate:%Y-%m-%d}'
+        self.assertLogFile(otherfn)
+        with open(self.fn, encoding="utf-8") as f:
+            for i, line in enumerate(f):
+                self.assertIn(f'testing2 {i}', line)
+        with open(otherfn, encoding="utf-8") as f:
+            for i, line in enumerate(f):
+                self.assertIn(f'testing1 {i}', line)
+
+    def test_rollover_at_weekday(self):
+        now = datetime.datetime.now()
+        atTime = now.time()
+        fmt = logging.Formatter('%(asctime)s %(message)s')
+        for i in range(3):
+            fh = logging.handlers.TimedRotatingFileHandler(
+                self.fn, encoding="utf-8", when=f'W{now.weekday()}', atTime=atTime)
+            fh.setFormatter(fmt)
+            r2 = logging.makeLogRecord({'msg': f'testing1 {i}'})
+            fh.emit(r2)
+            fh.close()
+        self.assertLogFile(self.fn)
+        with open(self.fn, encoding="utf-8") as f:
+            for i, line in enumerate(f):
+                self.assertIn(f'testing1 {i}', line)
+
+        os.utime(self.fn, (time.time() - 1,)*2)
+        for i in range(2):
+            fh = logging.handlers.TimedRotatingFileHandler(
+                self.fn, encoding="utf-8", when=f'W{now.weekday()}', atTime=atTime)
+            fh.setFormatter(fmt)
+            r2 = logging.makeLogRecord({'msg': f'testing2 {i}'})
+            fh.emit(r2)
+            fh.close()
+        rolloverDate = datetime.datetime.now() - datetime.timedelta(days=7)
+        otherfn = f'{self.fn}.{rolloverDate:%Y-%m-%d}'
+        self.assertLogFile(otherfn)
+        with open(self.fn, encoding="utf-8") as f:
+            for i, line in enumerate(f):
+                self.assertIn(f'testing2 {i}', line)
+        with open(otherfn, encoding="utf-8") as f:
+            for i, line in enumerate(f):
+                self.assertIn(f'testing1 {i}', line)
+
     def test_invalid(self):
         assertRaises = self.assertRaises
         assertRaises(ValueError, logging.handlers.TimedRotatingFileHandler,
@@ -5663,22 +5730,47 @@ class TimedRotatingFileHandlerTest(BaseFileTest):
         assertRaises(ValueError, logging.handlers.TimedRotatingFileHandler,
                      self.fn, 'W7', encoding="utf-8", delay=True)
 
+    # TODO: Test for utc=False.
     def test_compute_rollover_daily_attime(self):
         currentTime = 0
+        rh = logging.handlers.TimedRotatingFileHandler(
+            self.fn, encoding="utf-8", when='MIDNIGHT',
+            utc=True, atTime=None)
+        try:
+            actual = rh.computeRollover(currentTime)
+            self.assertEqual(actual, currentTime + 24 * 60 * 60)
+
+            actual = rh.computeRollover(currentTime + 24 * 60 * 60 - 1)
+            self.assertEqual(actual, currentTime + 24 * 60 * 60)
+
+            actual = rh.computeRollover(currentTime + 24 * 60 * 60)
+            self.assertEqual(actual, currentTime + 48 * 60 * 60)
+
+            actual = rh.computeRollover(currentTime + 25 * 60 * 60)
+            self.assertEqual(actual, currentTime + 48 * 60 * 60)
+        finally:
+            rh.close()
+
         atTime = datetime.time(12, 0, 0)
         rh = logging.handlers.TimedRotatingFileHandler(
-            self.fn, encoding="utf-8", when='MIDNIGHT', interval=1, backupCount=0,
+            self.fn, encoding="utf-8", when='MIDNIGHT',
             utc=True, atTime=atTime)
         try:
             actual = rh.computeRollover(currentTime)
             self.assertEqual(actual, currentTime + 12 * 60 * 60)
 
+            actual = rh.computeRollover(currentTime + 12 * 60 * 60 - 1)
+            self.assertEqual(actual, currentTime + 12 * 60 * 60)
+
+            actual = rh.computeRollover(currentTime + 12 * 60 * 60)
+            self.assertEqual(actual, currentTime + 36 * 60 * 60)
+
             actual = rh.computeRollover(currentTime + 13 * 60 * 60)
             self.assertEqual(actual, currentTime + 36 * 60 * 60)
         finally:
             rh.close()
 
-    #@unittest.skipIf(True, 'Temporarily skipped while failures investigated.')
+    # TODO: Test for utc=False.
     def test_compute_rollover_weekly_attime(self):
         currentTime = int(time.time())
         today = currentTime - currentTime % 86400
@@ -5703,14 +5795,28 @@ class TimedRotatingFileHandlerTest(BaseFileTest):
                 expected += 12 * 60 * 60
                 # Add in adjustment for today
                 expected += today
+
                 actual = rh.computeRollover(today)
                 if actual != expected:
                     print('failed in timezone: %d' % time.timezone)
                     print('local vars: %s' % locals())
                 self.assertEqual(actual, expected)
+
+                actual = rh.computeRollover(today + 12 * 60 * 60 - 1)
+                if actual != expected:
+                    print('failed in timezone: %d' % time.timezone)
+                    print('local vars: %s' % locals())
+                self.assertEqual(actual, expected)
+
                 if day == wday:
                     # goes into following week
                     expected += 7 * 24 * 60 * 60
+                actual = rh.computeRollover(today + 12 * 60 * 60)
+                if actual != expected:
+                    print('failed in timezone: %d' % time.timezone)
+                    print('local vars: %s' % locals())
+                self.assertEqual(actual, expected)
+
                 actual = rh.computeRollover(today + 13 * 60 * 60)
                 if actual != expected:
                     print('failed in timezone: %d' % time.timezone)
@@ -5805,6 +5911,244 @@ class TimedRotatingFileHandlerTest(BaseFileTest):
                 suffix = fn[(len(prefix)+1):]
                 self.assertRegex(suffix, matcher)
 
+    # Run with US-style DST rules: DST begins 2 a.m. on second Sunday in
+    # March (M3.2.0) and ends 2 a.m. on first Sunday in November (M11.1.0).
+    @support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0')
+    def test_compute_rollover_MIDNIGHT_local(self):
+        # DST begins at 2012-3-11T02:00:00 and ends at 2012-11-4T02:00:00.
+        DT = datetime.datetime
+        def test(current, expected):
+            actual = fh.computeRollover(current.timestamp())
+            diff = actual - expected.timestamp()
+            if diff:
+                self.assertEqual(diff, 0, datetime.timedelta(seconds=diff))
+
+        fh = logging.handlers.TimedRotatingFileHandler(
+            self.fn, encoding="utf-8", when='MIDNIGHT', utc=False)
+
+        test(DT(2012, 3, 10, 23, 59, 59), DT(2012, 3, 11, 0, 0))
+        test(DT(2012, 3, 11, 0, 0), DT(2012, 3, 12, 0, 0))
+        test(DT(2012, 3, 11, 1, 0), DT(2012, 3, 12, 0, 0))
+
+        test(DT(2012, 11, 3, 23, 59, 59), DT(2012, 11, 4, 0, 0))
+        test(DT(2012, 11, 4, 0, 0), DT(2012, 11, 5, 0, 0))
+        test(DT(2012, 11, 4, 1, 0), DT(2012, 11, 5, 0, 0))
+
+        fh.close()
+
+        fh = logging.handlers.TimedRotatingFileHandler(
+            self.fn, encoding="utf-8", when='MIDNIGHT', utc=False,
+            atTime=datetime.time(12, 0, 0))
+
+        test(DT(2012, 3, 10, 11, 59, 59), DT(2012, 3, 10, 12, 0))
+        test(DT(2012, 3, 10, 12, 0), DT(2012, 3, 11, 12, 0))
+        test(DT(2012, 3, 10, 13, 0), DT(2012, 3, 11, 12, 0))
+
+        test(DT(2012, 11, 3, 11, 59, 59), DT(2012, 11, 3, 12, 0))
+        test(DT(2012, 11, 3, 12, 0), DT(2012, 11, 4, 12, 0))
+        test(DT(2012, 11, 3, 13, 0), DT(2012, 11, 4, 12, 0))
+
+        fh.close()
+
+        fh = logging.handlers.TimedRotatingFileHandler(
+            self.fn, encoding="utf-8", when='MIDNIGHT', utc=False,
+            atTime=datetime.time(2, 0, 0))
+
+        test(DT(2012, 3, 10, 1, 59, 59), DT(2012, 3, 10, 2, 0))
+        # 2:00:00 is the same as 3:00:00 at 2012-3-11.
+        test(DT(2012, 3, 10, 2, 0), DT(2012, 3, 11, 3, 0))
+        test(DT(2012, 3, 10, 3, 0), DT(2012, 3, 11, 3, 0))
+
+        test(DT(2012, 3, 11, 1, 59, 59), DT(2012, 3, 11, 3, 0))
+        # No time between 2:00:00 and 3:00:00 at 2012-3-11.
+        test(DT(2012, 3, 11, 3, 0), DT(2012, 3, 12, 2, 0))
+        test(DT(2012, 3, 11, 4, 0), DT(2012, 3, 12, 2, 0))
+
+        test(DT(2012, 11, 3, 1, 59, 59), DT(2012, 11, 3, 2, 0))
+        test(DT(2012, 11, 3, 2, 0), DT(2012, 11, 4, 2, 0))
+        test(DT(2012, 11, 3, 3, 0), DT(2012, 11, 4, 2, 0))
+
+        # 1:00:00-2:00:00 is repeated twice at 2012-11-4.
+        test(DT(2012, 11, 4, 1, 59, 59), DT(2012, 11, 4, 2, 0))
+        test(DT(2012, 11, 4, 1, 59, 59, fold=1), DT(2012, 11, 4, 2, 0))
+        test(DT(2012, 11, 4, 2, 0), DT(2012, 11, 5, 2, 0))
+        test(DT(2012, 11, 4, 3, 0), DT(2012, 11, 5, 2, 0))
+
+        fh.close()
+
+        fh = logging.handlers.TimedRotatingFileHandler(
+            self.fn, encoding="utf-8", when='MIDNIGHT', utc=False,
+            atTime=datetime.time(2, 30, 0))
+
+        test(DT(2012, 3, 10, 2, 29, 59), DT(2012, 3, 10, 2, 30))
+        # No time 2:30:00 at 2012-3-11.
+        test(DT(2012, 3, 10, 2, 30), DT(2012, 3, 11, 3, 30))
+        test(DT(2012, 3, 10, 3, 0), DT(2012, 3, 11, 3, 30))
+
+        test(DT(2012, 3, 11, 1, 59, 59), DT(2012, 3, 11, 3, 30))
+        # No time between 2:00:00 and 3:00:00 at 2012-3-11.
+        test(DT(2012, 3, 11, 3, 0), DT(2012, 3, 12, 2, 30))
+        test(DT(2012, 3, 11, 3, 30), DT(2012, 3, 12, 2, 30))
+
+        test(DT(2012, 11, 3, 2, 29, 59), DT(2012, 11, 3, 2, 30))
+        test(DT(2012, 11, 3, 2, 30), DT(2012, 11, 4, 2, 30))
+        test(DT(2012, 11, 3, 3, 0), DT(2012, 11, 4, 2, 30))
+
+        fh.close()
+
+        fh = logging.handlers.TimedRotatingFileHandler(
+            self.fn, encoding="utf-8", when='MIDNIGHT', utc=False,
+            atTime=datetime.time(1, 30, 0))
+
+        test(DT(2012, 3, 11, 1, 29, 59), DT(2012, 3, 11, 1, 30))
+        test(DT(2012, 3, 11, 1, 30), DT(2012, 3, 12, 1, 30))
+        test(DT(2012, 3, 11, 1, 59, 59), DT(2012, 3, 12, 1, 30))
+        # No time between 2:00:00 and 3:00:00 at 2012-3-11.
+        test(DT(2012, 3, 11, 3, 0), DT(2012, 3, 12, 1, 30))
+        test(DT(2012, 3, 11, 3, 30), DT(2012, 3, 12, 1, 30))
+
+        # 1:00:00-2:00:00 is repeated twice at 2012-11-4.
+        test(DT(2012, 11, 4, 1, 0), DT(2012, 11, 4, 1, 30))
+        test(DT(2012, 11, 4, 1, 29, 59), DT(2012, 11, 4, 1, 30))
+        test(DT(2012, 11, 4, 1, 30), DT(2012, 11, 5, 1, 30))
+        test(DT(2012, 11, 4, 1, 59, 59), DT(2012, 11, 5, 1, 30))
+        # It is weird, but the rollover date jumps back from 2012-11-5
+        # to 2012-11-4.
+        test(DT(2012, 11, 4, 1, 0, fold=1), DT(2012, 11, 4, 1, 30, fold=1))
+        test(DT(2012, 11, 4, 1, 29, 59, fold=1), DT(2012, 11, 4, 1, 30, fold=1))
+        test(DT(2012, 11, 4, 1, 30, fold=1), DT(2012, 11, 5, 1, 30))
+        test(DT(2012, 11, 4, 1, 59, 59, fold=1), DT(2012, 11, 5, 1, 30))
+        test(DT(2012, 11, 4, 2, 0), DT(2012, 11, 5, 1, 30))
+        test(DT(2012, 11, 4, 2, 30), DT(2012, 11, 5, 1, 30))
+
+        fh.close()
+
+    # Run with US-style DST rules: DST begins 2 a.m. on second Sunday in
+    # March (M3.2.0) and ends 2 a.m. on first Sunday in November (M11.1.0).
+    @support.run_with_tz('EST+05EDT,M3.2.0,M11.1.0')
+    def test_compute_rollover_W6_local(self):
+        # DST begins at 2012-3-11T02:00:00 and ends at 2012-11-4T02:00:00.
+        DT = datetime.datetime
+        def test(current, expected):
+            actual = fh.computeRollover(current.timestamp())
+            diff = actual - expected.timestamp()
+            if diff:
+                self.assertEqual(diff, 0, datetime.timedelta(seconds=diff))
+
+        fh = logging.handlers.TimedRotatingFileHandler(
+            self.fn, encoding="utf-8", when='W6', utc=False)
+
+        test(DT(2012, 3, 4, 23, 59, 59), DT(2012, 3, 5, 0, 0))
+        test(DT(2012, 3, 5, 0, 0), DT(2012, 3, 12, 0, 0))
+        test(DT(2012, 3, 5, 1, 0), DT(2012, 3, 12, 0, 0))
+
+        test(DT(2012, 10, 28, 23, 59, 59), DT(2012, 10, 29, 0, 0))
+        test(DT(2012, 10, 29, 0, 0), DT(2012, 11, 5, 0, 0))
+        test(DT(2012, 10, 29, 1, 0), DT(2012, 11, 5, 0, 0))
+
+        fh.close()
+
+        fh = logging.handlers.TimedRotatingFileHandler(
+            self.fn, encoding="utf-8", when='W6', utc=False,
+            atTime=datetime.time(0, 0, 0))
+
+        test(DT(2012, 3, 10, 23, 59, 59), DT(2012, 3, 11, 0, 0))
+        test(DT(2012, 3, 11, 0, 0), DT(2012, 3, 18, 0, 0))
+        test(DT(2012, 3, 11, 1, 0), DT(2012, 3, 18, 0, 0))
+
+        test(DT(2012, 11, 3, 23, 59, 59), DT(2012, 11, 4, 0, 0))
+        test(DT(2012, 11, 4, 0, 0), DT(2012, 11, 11, 0, 0))
+        test(DT(2012, 11, 4, 1, 0), DT(2012, 11, 11, 0, 0))
+
+        fh.close()
+
+        fh = logging.handlers.TimedRotatingFileHandler(
+            self.fn, encoding="utf-8", when='W6', utc=False,
+            atTime=datetime.time(12, 0, 0))
+
+        test(DT(2012, 3, 4, 11, 59, 59), DT(2012, 3, 4, 12, 0))
+        test(DT(2012, 3, 4, 12, 0), DT(2012, 3, 11, 12, 0))
+        test(DT(2012, 3, 4, 13, 0), DT(2012, 3, 11, 12, 0))
+
+        test(DT(2012, 10, 28, 11, 59, 59), DT(2012, 10, 28, 12, 0))
+        test(DT(2012, 10, 28, 12, 0), DT(2012, 11, 4, 12, 0))
+        test(DT(2012, 10, 28, 13, 0), DT(2012, 11, 4, 12, 0))
+
+        fh.close()
+
+        fh = logging.handlers.TimedRotatingFileHandler(
+            self.fn, encoding="utf-8", when='W6', utc=False,
+            atTime=datetime.time(2, 0, 0))
+
+        test(DT(2012, 3, 4, 1, 59, 59), DT(2012, 3, 4, 2, 0))
+        # 2:00:00 is the same as 3:00:00 at 2012-3-11.
+        test(DT(2012, 3, 4, 2, 0), DT(2012, 3, 11, 3, 0))
+        test(DT(2012, 3, 4, 3, 0), DT(2012, 3, 11, 3, 0))
+
+        test(DT(2012, 3, 11, 1, 59, 59), DT(2012, 3, 11, 3, 0))
+        # No time between 2:00:00 and 3:00:00 at 2012-3-11.
+        test(DT(2012, 3, 11, 3, 0), DT(2012, 3, 18, 2, 0))
+        test(DT(2012, 3, 11, 4, 0), DT(2012, 3, 18, 2, 0))
+
+        test(DT(2012, 10, 28, 1, 59, 59), DT(2012, 10, 28, 2, 0))
+        test(DT(2012, 10, 28, 2, 0), DT(2012, 11, 4, 2, 0))
+        test(DT(2012, 10, 28, 3, 0), DT(2012, 11, 4, 2, 0))
+
+        # 1:00:00-2:00:00 is repeated twice at 2012-11-4.
+        test(DT(2012, 11, 4, 1, 59, 59), DT(2012, 11, 4, 2, 0))
+        test(DT(2012, 11, 4, 1, 59, 59, fold=1), DT(2012, 11, 4, 2, 0))
+        test(DT(2012, 11, 4, 2, 0), DT(2012, 11, 11, 2, 0))
+        test(DT(2012, 11, 4, 3, 0), DT(2012, 11, 11, 2, 0))
+
+        fh.close()
+
+        fh = logging.handlers.TimedRotatingFileHandler(
+            self.fn, encoding="utf-8", when='W6', utc=False,
+            atTime=datetime.time(2, 30, 0))
+
+        test(DT(2012, 3, 4, 2, 29, 59), DT(2012, 3, 4, 2, 30))
+        # No time 2:30:00 at 2012-3-11.
+        test(DT(2012, 3, 4, 2, 30), DT(2012, 3, 11, 3, 30))
+        test(DT(2012, 3, 4, 3, 0), DT(2012, 3, 11, 3, 30))
+
+        test(DT(2012, 3, 11, 1, 59, 59), DT(2012, 3, 11, 3, 30))
+        # No time between 2:00:00 and 3:00:00 at 2012-3-11.
+        test(DT(2012, 3, 11, 3, 0), DT(2012, 3, 18, 2, 30))
+        test(DT(2012, 3, 11, 3, 30), DT(2012, 3, 18, 2, 30))
+
+        test(DT(2012, 10, 28, 2, 29, 59), DT(2012, 10, 28, 2, 30))
+        test(DT(2012, 10, 28, 2, 30), DT(2012, 11, 4, 2, 30))
+        test(DT(2012, 10, 28, 3, 0), DT(2012, 11, 4, 2, 30))
+
+        fh.close()
+
+        fh = logging.handlers.TimedRotatingFileHandler(
+            self.fn, encoding="utf-8", when='W6', utc=False,
+            atTime=datetime.time(1, 30, 0))
+
+        test(DT(2012, 3, 11, 1, 29, 59), DT(2012, 3, 11, 1, 30))
+        test(DT(2012, 3, 11, 1, 30), DT(2012, 3, 18, 1, 30))
+        test(DT(2012, 3, 11, 1, 59, 59), DT(2012, 3, 18, 1, 30))
+        # No time between 2:00:00 and 3:00:00 at 2012-3-11.
+        test(DT(2012, 3, 11, 3, 0), DT(2012, 3, 18, 1, 30))
+        test(DT(2012, 3, 11, 3, 30), DT(2012, 3, 18, 1, 30))
+
+        # 1:00:00-2:00:00 is repeated twice at 2012-11-4.
+        test(DT(2012, 11, 4, 1, 0), DT(2012, 11, 4, 1, 30))
+        test(DT(2012, 11, 4, 1, 29, 59), DT(2012, 11, 4, 1, 30))
+        test(DT(2012, 11, 4, 1, 30), DT(2012, 11, 11, 1, 30))
+        test(DT(2012, 11, 4, 1, 59, 59), DT(2012, 11, 11, 1, 30))
+        # It is weird, but the rollover date jumps back from 2012-11-11
+        # to 2012-11-4.
+        test(DT(2012, 11, 4, 1, 0, fold=1), DT(2012, 11, 4, 1, 30, fold=1))
+        test(DT(2012, 11, 4, 1, 29, 59, fold=1), DT(2012, 11, 4, 1, 30, fold=1))
+        test(DT(2012, 11, 4, 1, 30, fold=1), DT(2012, 11, 11, 1, 30))
+        test(DT(2012, 11, 4, 1, 59, 59, fold=1), DT(2012, 11, 11, 1, 30))
+        test(DT(2012, 11, 4, 2, 0), DT(2012, 11, 11, 1, 30))
+        test(DT(2012, 11, 4, 2, 30), DT(2012, 11, 11, 1, 30))
+
+        fh.close()
+
 def secs(**kw):
     return datetime.timedelta(**kw) // datetime.timedelta(seconds=1)
 
diff --git a/Misc/NEWS.d/next/Library/2024-03-01-11-57-32.gh-issue-88352.bZ68rw.rst b/Misc/NEWS.d/next/Library/2024-03-01-11-57-32.gh-issue-88352.bZ68rw.rst
new file mode 100644 (file)
index 0000000..8ad4ff7
--- /dev/null
@@ -0,0 +1,6 @@
+Fix the computation of the next rollover time in the
+:class:`logging.TimedRotatingFileHandler` handler. :meth:`!computeRollover`
+now always returns a timestamp larger than the specified time and works
+correctly during the DST change. :meth:`!doRollover` no longer overwrite the
+already rolled over file, saving from data loss when run at midnight or
+during repeated time at the DST change.