Skip to content

Commit a4b8ab9

Browse files
committed
gh-159184: Make the marshal order of set elements with equal dumps stable
Set elements with the same dump (NaNs which are distinct objects) were written in set iteration order, which depends on their addresses. When one of them is also referenced elsewhere in the dump, the position of the reference among them changed from run to run. Elements with equal dumps are now ordered by when they were first written in this dump; floats and complex numbers not written yet are interchangeable, and which one is which is decided when one of them is referenced again. This relies on the writer knowing exactly which objects it has written (gh-129724). The elements are sorted with qsort() on the bytes of their dumps, in an array instead of a list of tuples.
1 parent 6ee27e0 commit a4b8ab9

3 files changed

Lines changed: 324 additions & 79 deletions

File tree

‎Lib/test/test_marshal.py‎

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -617,7 +617,6 @@ def test_unorderable_set_items(self):
617617
self.assertEqual(sorted(map(repr, new)),
618618
sorted(map(repr, sample)))
619619

620-
621620
def test_unorderable_set_items_in_code(self):
622621
# A constant set containing NaNs which are distinct objects.
623622
# Run in a subprocess: constants of code objects are immortal
@@ -662,7 +661,6 @@ def shrink(sample):
662661
"set changed size"):
663662
marshal.dumps(sample, version)
664663

665-
666664
def test_error_in_buffer(self):
667665
# The BufferError raised for a non-contiguous buffer is not replaced
668666
# with a generic error.
@@ -1238,7 +1236,8 @@ def test_nan_in_set(self):
12381236
nan1 = float('nan')
12391237
nan2 = float('nan')
12401238
nan3 = float('nan')
1241-
s = kind([nan1, 1.5, nan2, nan3])
1239+
s = kind([nan1, 1.5, nan2, nan3,
1240+
complex('nan'), complex('nan')])
12421241
for sample in (s, [s], [nan1, s], [s, nan2],
12431242
[nan3, s, nan1], [nan1, nan2, nan3, s],
12441243
[s, s], (nan2, [s, nan2], s)):
@@ -1247,6 +1246,59 @@ def test_nan_in_set(self):
12471246
new = marshal.loads(data)
12481247
self.check_same_structure(new, sample, version >= 3)
12491248

1249+
@support.cpython_only
1250+
def test_nan_in_set_order(self):
1251+
# The output does not depend on the order of the elements of a set
1252+
# which have the same marshalled representation, even if some of
1253+
# them are also referenced elsewhere.
1254+
version = self.VERSION
1255+
def flagged(data):
1256+
data = bytearray(data)
1257+
data[0] |= self.FLAG_REF
1258+
return bytes(data)
1259+
def ref(index):
1260+
return b'r' + struct.pack('<i', index)
1261+
keep = []
1262+
for make_nan in (lambda: float('nan'), lambda: complex('nan')):
1263+
data = marshal.dumps(make_nan(), version)
1264+
fdata = flagged(data)
1265+
data15 = marshal.dumps(1.5, version)
1266+
for kind, typecode in (set, b'<'), (frozenset, b'>'):
1267+
head = typecode + b'\4\0\0\0' + data15
1268+
for i in range(20):
1269+
# Different objects at different addresses every time.
1270+
keep.append(bytes(i * 8))
1271+
nans = [make_nan() for j in range(3)]
1272+
keep.append(nans)
1273+
s = kind([1.5, *nans])
1274+
a, b, c = nans[i % 3:] + nans[:i % 3]
1275+
for sample, expected in [
1276+
(s, head + data * 3),
1277+
([s, a],
1278+
b'[\2\0\0\0' + head + fdata + data * 2 + ref(0)),
1279+
([a, s],
1280+
b'[\2\0\0\0' + fdata + head + ref(0) + data * 2),
1281+
([s, a, b, a],
1282+
b'[\4\0\0\0' + head + fdata * 2 + data +
1283+
ref(0) + ref(1) + ref(0)),
1284+
([a, b, s, c],
1285+
b'[\4\0\0\0' + fdata * 2 + head + ref(0) + ref(1) +
1286+
flagged(data) + ref(2)),
1287+
([b, s, c, a, kind([a, b, c])],
1288+
b'[\5\0\0\0' + fdata + head + ref(0) + fdata * 2 +
1289+
ref(1) + ref(2) + typecode + b'\3\0\0\0' +
1290+
ref(0) + ref(1) + ref(2)),
1291+
([s, kind([b, a]), kind([c, make_nan()]), b, a, c],
1292+
b'[\6\0\0\0' + head + fdata * 3 +
1293+
typecode + b'\2\0\0\0' + ref(0) + ref(1) +
1294+
typecode + b'\2\0\0\0' + ref(2) + data +
1295+
ref(0) + ref(1) + ref(2)),
1296+
]:
1297+
self.assertEqual(marshal.dumps(sample, version),
1298+
expected)
1299+
new = marshal.loads(expected)
1300+
self.check_same_structure(new, sample, True)
1301+
12501302
def check_same_structure(self, new, sample, shared):
12511303
# Both object graphs have the same values and, if shared is true,
12521304
# share the same objects. Elements of sets are matched in any
@@ -1261,7 +1313,7 @@ def known(x):
12611313
if shared:
12621314
return seen.get(id(x), -1)
12631315
def visit(x):
1264-
if isinstance(x, float):
1316+
if isinstance(x, (float, complex)):
12651317
result.append((repr(x), number(x)))
12661318
elif isinstance(x, (set, frozenset)):
12671319
# Elements are only numbered if they occur elsewhere.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
The output of :func:`marshal.dumps` no longer depends on the order of the
2+
elements of a set which have the same marshalled form, such as NaNs which
3+
are distinct objects.

0 commit comments

Comments
 (0)