Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
244 changes: 241 additions & 3 deletions Lib/test/test_structseq.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import copy
import gc
import os
import pickle
import re
import textwrap
import time
import unittest
from test.support import script_helper


class StructSeqTest(unittest.TestCase):
Expand Down Expand Up @@ -37,7 +43,7 @@ def test_repr(self):
# os.stat() gives a complicated struct sequence.
st = os.stat(__file__)
rep = repr(st)
self.assertTrue(rep.startswith("os.stat_result"))
self.assertStartsWith(rep, "os.stat_result")
self.assertIn("st_mode=", rep)
self.assertIn("st_ino=", rep)
self.assertIn("st_dev=", rep)
Expand Down Expand Up @@ -81,6 +87,7 @@ def test_fields(self):
self.assertEqual(t.n_unnamed_fields, 0)
self.assertEqual(t.n_fields, time._STRUCT_TM_ITEMS)

@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: Unexpected keyword argument dict
def test_constructor(self):
t = time.struct_time

Expand All @@ -89,10 +96,72 @@ def test_constructor(self):
self.assertRaises(TypeError, t, "123")
self.assertRaises(TypeError, t, "123", dict={})
self.assertRaises(TypeError, t, "123456789", dict=None)
self.assertRaises(TypeError, t, seq="123456789", dict={})

self.assertEqual(t("123456789"), tuple("123456789"))
self.assertEqual(t("123456789", {}), tuple("123456789"))
self.assertEqual(t("123456789", dict={}), tuple("123456789"))
self.assertEqual(t(sequence="123456789", dict={}), tuple("123456789"))

self.assertEqual(t("1234567890"), tuple("123456789"))
self.assertEqual(t("1234567890").tm_zone, "0")
self.assertEqual(t("123456789", {"tm_zone": "some zone"}), tuple("123456789"))
self.assertEqual(t("123456789", {"tm_zone": "some zone"}).tm_zone, "some zone")

s = "123456789"
self.assertEqual("".join(t(s)), s)

@unittest.expectedFailure # TODO: RUSTPYTHON; Wrong error message
def test_constructor_with_duplicate_fields(self):
t = time.struct_time

error_message = re.escape("got duplicate or unexpected field name(s)")
with self.assertRaisesRegex(TypeError, error_message):
t("1234567890", dict={"tm_zone": "some zone"})
with self.assertRaisesRegex(TypeError, error_message):
t("1234567890", dict={"tm_zone": "some zone", "tm_mon": 1})
with self.assertRaisesRegex(TypeError, error_message):
t("1234567890", dict={"error": 0, "tm_zone": "some zone"})
with self.assertRaisesRegex(TypeError, error_message):
t("1234567890", dict={"error": 0, "tm_zone": "some zone", "tm_mon": 1})

@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: expected at most 1 arguments, got 2
def test_constructor_with_duplicate_unnamed_fields(self):
assert os.stat_result.n_unnamed_fields > 0
n_visible_fields = os.stat_result.n_sequence_fields

r = os.stat_result(range(n_visible_fields), {'st_atime': -1.0})
self.assertEqual(r.st_atime, -1.0)
self.assertEqual(r, tuple(range(n_visible_fields)))

r = os.stat_result((*range(n_visible_fields), -1.0))
self.assertEqual(r.st_atime, -1.0)
self.assertEqual(r, tuple(range(n_visible_fields)))

with self.assertRaisesRegex(TypeError,
re.escape("got duplicate or unexpected field name(s)")):
os.stat_result((*range(n_visible_fields), -1.0), {'st_atime': -1.0})

@unittest.expectedFailure # TODO: RUSTPYTHON; Wrong error message
def test_constructor_with_unknown_fields(self):
t = time.struct_time

error_message = re.escape("got duplicate or unexpected field name(s)")
with self.assertRaisesRegex(TypeError, error_message):
t("123456789", dict={"tm_year": 0})
with self.assertRaisesRegex(TypeError, error_message):
t("123456789", dict={"tm_year": 0, "tm_mon": 1})
with self.assertRaisesRegex(TypeError, error_message):
t("123456789", dict={"tm_zone": "some zone", "tm_mon": 1})
with self.assertRaisesRegex(TypeError, error_message):
t("123456789", dict={"tm_zone": "some zone", "error": 0})
with self.assertRaisesRegex(TypeError, error_message):
t("123456789", dict={"error": 0, "tm_zone": "some zone", "tm_mon": 1})
with self.assertRaisesRegex(TypeError, error_message):
t("123456789", dict={"error": 0})
with self.assertRaisesRegex(TypeError, error_message):
t("123456789", dict={"tm_zone": "some zone", "error": 0})

def test_eviltuple(self):
class Exc(Exception):
pass
Expand All @@ -106,9 +175,80 @@ def __len__(self):

self.assertRaises(Exc, time.struct_time, C())

def test_reduce(self):
def test_pickling(self):
t = time.gmtime()
x = t.__reduce__()
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
p = pickle.dumps(t, proto)
t2 = pickle.loads(p)
self.assertEqual(t2.__class__, t.__class__)
self.assertEqual(t2, t)
self.assertEqual(t2.tm_year, t.tm_year)
self.assertEqual(t2.tm_zone, t.tm_zone)

@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: expected at most 1 arguments, got 2
def test_pickling_with_unnamed_fields(self):
assert os.stat_result.n_unnamed_fields > 0

r = os.stat_result(range(os.stat_result.n_sequence_fields),
{'st_atime': 1.0, 'st_atime_ns': 2.0})
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
p = pickle.dumps(r, proto)
r2 = pickle.loads(p)
self.assertEqual(r2.__class__, r.__class__)
self.assertEqual(r2, r)
self.assertEqual(r2.st_mode, r.st_mode)
self.assertEqual(r2.st_atime, r.st_atime)
self.assertEqual(r2.st_atime_ns, r.st_atime_ns)

def test_copying(self):
n_fields = time.struct_time.n_fields
t = time.struct_time([[i] for i in range(n_fields)])

t2 = copy.copy(t)
self.assertEqual(t2.__class__, t.__class__)
self.assertEqual(t2, t)
self.assertEqual(t2.tm_year, t.tm_year)
self.assertEqual(t2.tm_zone, t.tm_zone)
self.assertIs(t2[0], t[0])
self.assertIs(t2.tm_year, t.tm_year)

t3 = copy.deepcopy(t)
self.assertEqual(t3.__class__, t.__class__)
self.assertEqual(t3, t)
self.assertEqual(t3.tm_year, t.tm_year)
self.assertEqual(t3.tm_zone, t.tm_zone)
self.assertIsNot(t3[0], t[0])
self.assertIsNot(t3.tm_year, t.tm_year)

@unittest.expectedFailure # TODO: RUSTPYTHON; TypeError: expected at most 1 arguments, got 2
def test_copying_with_unnamed_fields(self):
assert os.stat_result.n_unnamed_fields > 0

n_sequence_fields = os.stat_result.n_sequence_fields
r = os.stat_result([[i] for i in range(n_sequence_fields)],
{'st_atime': [1.0], 'st_atime_ns': [2.0]})

r2 = copy.copy(r)
self.assertEqual(r2.__class__, r.__class__)
self.assertEqual(r2, r)
self.assertEqual(r2.st_mode, r.st_mode)
self.assertEqual(r2.st_atime, r.st_atime)
self.assertEqual(r2.st_atime_ns, r.st_atime_ns)
self.assertIs(r2[0], r[0])
self.assertIs(r2.st_mode, r.st_mode)
self.assertIs(r2.st_atime, r.st_atime)
self.assertIs(r2.st_atime_ns, r.st_atime_ns)

r3 = copy.deepcopy(r)
self.assertEqual(r3.__class__, r.__class__)
self.assertEqual(r3, r)
self.assertEqual(r3.st_mode, r.st_mode)
self.assertEqual(r3.st_atime, r.st_atime)
self.assertEqual(r3.st_atime_ns, r.st_atime_ns)
self.assertIsNot(r3[0], r[0])
self.assertIsNot(r3.st_mode, r.st_mode)
self.assertIsNot(r3.st_atime, r.st_atime)
self.assertIsNot(r3.st_atime_ns, r.st_atime_ns)

def test_extended_getslice(self):
# Test extended slicing by comparing with list slicing.
Expand All @@ -133,6 +273,104 @@ def test_match_args_with_unnamed_fields(self):
self.assertEqual(os.stat_result.n_unnamed_fields, 3)
self.assertEqual(os.stat_result.__match_args__, expected_args)

def test_copy_replace_all_fields_visible(self):
assert os.times_result.n_unnamed_fields == 0
assert os.times_result.n_sequence_fields == os.times_result.n_fields

t = os.times()

# visible fields
self.assertEqual(copy.replace(t), t)
self.assertIsInstance(copy.replace(t), os.times_result)
self.assertEqual(copy.replace(t, user=1.5), (1.5, *t[1:]))
self.assertEqual(copy.replace(t, system=2.5), (t[0], 2.5, *t[2:]))
self.assertEqual(copy.replace(t, user=1.5, system=2.5), (1.5, 2.5, *t[2:]))

# unknown fields
with self.assertRaisesRegex(TypeError, 'unexpected field name'):
copy.replace(t, error=-1)
with self.assertRaisesRegex(TypeError, 'unexpected field name'):
copy.replace(t, user=1, error=-1)

@unittest.expectedFailure # TODO: RUSTPYTHON; Wrong error message
def test_copy_replace_with_invisible_fields(self):
assert time.struct_time.n_unnamed_fields == 0
assert time.struct_time.n_sequence_fields < time.struct_time.n_fields

t = time.gmtime(0)

# visible fields
t2 = copy.replace(t)
self.assertEqual(t2, (1970, 1, 1, 0, 0, 0, 3, 1, 0))
self.assertIsInstance(t2, time.struct_time)
t3 = copy.replace(t, tm_year=2000)
self.assertEqual(t3, (2000, 1, 1, 0, 0, 0, 3, 1, 0))
self.assertEqual(t3.tm_year, 2000)
t4 = copy.replace(t, tm_mon=2)
self.assertEqual(t4, (1970, 2, 1, 0, 0, 0, 3, 1, 0))
self.assertEqual(t4.tm_mon, 2)
t5 = copy.replace(t, tm_year=2000, tm_mon=2)
self.assertEqual(t5, (2000, 2, 1, 0, 0, 0, 3, 1, 0))
self.assertEqual(t5.tm_year, 2000)
self.assertEqual(t5.tm_mon, 2)

# named invisible fields
self.assertHasAttr(t, 'tm_zone')
with self.assertRaisesRegex(AttributeError, 'readonly attribute'):
t.tm_zone = 'some other zone'
self.assertEqual(t2.tm_zone, t.tm_zone)
self.assertEqual(t3.tm_zone, t.tm_zone)
self.assertEqual(t4.tm_zone, t.tm_zone)
t6 = copy.replace(t, tm_zone='some other zone')
self.assertEqual(t, t6)
self.assertEqual(t6.tm_zone, 'some other zone')
t7 = copy.replace(t, tm_year=2000, tm_zone='some other zone')
self.assertEqual(t7, (2000, 1, 1, 0, 0, 0, 3, 1, 0))
self.assertEqual(t7.tm_year, 2000)
self.assertEqual(t7.tm_zone, 'some other zone')

# unknown fields
with self.assertRaisesRegex(TypeError, 'unexpected field name'):
copy.replace(t, error=2)
with self.assertRaisesRegex(TypeError, 'unexpected field name'):
copy.replace(t, tm_year=2000, error=2)
with self.assertRaisesRegex(TypeError, 'unexpected field name'):
copy.replace(t, tm_zone='some other zone', error=2)

def test_copy_replace_with_unnamed_fields(self):
assert os.stat_result.n_unnamed_fields > 0

r = os.stat_result(range(os.stat_result.n_sequence_fields))

error_message = re.escape('__replace__() is not supported')
with self.assertRaisesRegex(TypeError, error_message):
copy.replace(r)
with self.assertRaisesRegex(TypeError, error_message):
copy.replace(r, st_mode=1)
with self.assertRaisesRegex(TypeError, error_message):
copy.replace(r, error=2)
with self.assertRaisesRegex(TypeError, error_message):
copy.replace(r, st_mode=1, error=2)

def test_reference_cycle(self):
# gh-122527: Check that a structseq that's part of a reference cycle
# with its own type doesn't crash. Previously, if the type's dictionary
# was cleared first, the structseq instance would crash in the
# destructor.
script_helper.assert_python_ok("-c", textwrap.dedent(r"""
import time
t = time.gmtime()
type(t).refcyle = t
"""))

def test_replace_gc_tracked(self):
# Verify that __replace__ results are properly GC-tracked
time_struct = time.gmtime(0)
lst = []
replaced_struct = time_struct.__replace__(tm_year=lst)
lst.append(replaced_struct)

self.assertTrue(gc.is_tracked(replaced_struct))

if __name__ == "__main__":
unittest.main()
Loading
Loading