1+ import _imp
12import ast
23import errno
34import glob
@@ -1124,13 +1125,37 @@ def test_read_pyc_success(self, tmp_path: Path, pytester: Pytester) -> None:
11241125 _write_pyc (state , co , source_stat , hash , pyc )
11251126 assert _read_pyc (fn , pyc , state .trace ) is not None
11261127
1127- # pyc read should still work if only the mtime changed
1128- # Fallback to hash comparison
1129- new_mtime = source_stat .st_mtime + 1.2
1130- os .utime (fn , (new_mtime , new_mtime ))
1131- assert source_stat .st_mtime != os .stat (fn ).st_mtime
1128+ pyc_bytes = pyc .read_bytes ()
1129+ assert pyc_bytes [4 ] == 0 # timestamp flag set
1130+
1131+ def test_read_pyc_success_hash (self , tmp_path : Path , pytester : Pytester ) -> None :
1132+ from _pytest .assertion import AssertionState
1133+ from _pytest .assertion .rewrite import _read_pyc
1134+ from _pytest .assertion .rewrite import _rewrite_test
1135+ from _pytest .assertion .rewrite import _write_pyc
1136+
1137+ config = pytester .parseconfig ("--invalidation-mode=checked-hash" )
1138+ state = AssertionState (config , "rewrite" )
1139+
1140+ fn = tmp_path / "source.py"
1141+ pyc = Path (str (fn ) + "c" )
1142+
1143+ # Test private attribute didn't change
1144+ assert getattr (_imp , "check_hash_based_pycs" , None ) in {
1145+ "default" ,
1146+ "always" ,
1147+ "never" ,
1148+ }
1149+
1150+ fn .write_text ("def test(): assert True" , encoding = "utf-8" )
1151+ source_stat , hash , co = _rewrite_test (fn , config )
1152+ _write_pyc (state , co , source_stat , hash , pyc )
11321153 assert _read_pyc (fn , pyc , state .trace ) is not None
11331154
1155+ pyc_bytes = pyc .read_bytes ()
1156+ assert pyc_bytes [4 ] == 3 # checked-hash flag set
1157+ assert pyc_bytes [8 :16 ] == hash
1158+
11341159 def test_read_pyc_more_invalid (self , tmp_path : Path ) -> None :
11351160 from _pytest .assertion .rewrite import _read_pyc
11361161
@@ -1149,36 +1174,78 @@ def test_read_pyc_more_invalid(self, tmp_path: Path) -> None:
11491174 os .utime (source , (mtime_int , mtime_int ))
11501175
11511176 size = len (source_bytes ).to_bytes (4 , "little" )
1152- # source_hash returns bytes not int: https://github.com/python/typeshed/pull/10686
1153- hash : bytes = source_hash (source_bytes ) # type: ignore[assignment]
1154- hash = hash [:8 ]
11551177
11561178 code = marshal .dumps (compile (source_bytes , str (source ), "exec" ))
11571179
11581180 # Good header.
1159- pyc .write_bytes (magic + flags + mtime + size + hash + code )
1181+ pyc .write_bytes (magic + flags + mtime + size + code )
11601182 assert _read_pyc (source , pyc , print ) is not None
11611183
11621184 # Too short.
11631185 pyc .write_bytes (magic + flags + mtime )
11641186 assert _read_pyc (source , pyc , print ) is None
11651187
11661188 # Bad magic.
1167- pyc .write_bytes (b"\x12 \x34 \x56 \x78 " + flags + mtime + size + hash + code )
1189+ pyc .write_bytes (b"\x12 \x34 \x56 \x78 " + flags + mtime + size + code )
11681190 assert _read_pyc (source , pyc , print ) is None
11691191
11701192 # Unsupported flags.
1171- pyc .write_bytes (magic + b"\x00 \xff \x00 \x00 " + mtime + size + hash + code )
1193+ pyc .write_bytes (magic + b"\x00 \xff \x00 \x00 " + mtime + size + code )
11721194 assert _read_pyc (source , pyc , print ) is None
11731195
1174- # Bad size .
1175- pyc .write_bytes (magic + flags + mtime + b"\x99 \x00 \x00 \x00 " + hash + code )
1196+ # Bad mtime .
1197+ pyc .write_bytes (magic + flags + b"\x58 \x3d \xb0 \x5f " + size + code )
11761198 assert _read_pyc (source , pyc , print ) is None
11771199
1178- # Bad mtime + bad hash .
1179- pyc .write_bytes (magic + flags + b" \x58 \x3d \xb0 \x5f " + size + b"\x00 " * 8 + code )
1200+ # Bad size .
1201+ pyc .write_bytes (magic + flags + mtime + b"\x99 \ x00\x00 \x00 " + code )
11801202 assert _read_pyc (source , pyc , print ) is None
11811203
1204+ def test_read_pyc_more_invalid_hash (self , tmp_path : Path ) -> None :
1205+ from _pytest .assertion .rewrite import _read_pyc
1206+
1207+ source = tmp_path / "source.py"
1208+ pyc = tmp_path / "source.pyc"
1209+
1210+ source_bytes = b"def test(): pass\n "
1211+ source .write_bytes (source_bytes )
1212+
1213+ magic = importlib .util .MAGIC_NUMBER
1214+
1215+ flags = b"\x00 \x00 \x00 \x00 "
1216+ flags_hash = b"\x03 \x00 \x00 \x00 "
1217+
1218+ mtime = b"\x58 \x3c \xb0 \x5f "
1219+ mtime_int = int .from_bytes (mtime , "little" )
1220+ os .utime (source , (mtime_int , mtime_int ))
1221+
1222+ size = len (source_bytes ).to_bytes (4 , "little" )
1223+
1224+ # source_hash returns bytes not int: https://github.com/python/typeshed/pull/10686
1225+ hash : bytes = source_hash (source_bytes ) # type: ignore[assignment]
1226+ hash = hash [:8 ]
1227+
1228+ code = marshal .dumps (compile (source_bytes , str (source ), "exec" ))
1229+
1230+ # check_hash_based_pycs == "default" with hash based pyc file.
1231+ pyc .write_bytes (magic + flags_hash + hash + code )
1232+ assert _read_pyc (source , pyc , print ) is not None
1233+
1234+ # check_hash_based_pycs == "always" with hash based pyc file.
1235+ with mock .patch .object (_imp , "check_hash_based_pycs" , "always" ):
1236+ pyc .write_bytes (magic + flags_hash + hash + code )
1237+ assert _read_pyc (source , pyc , print ) is not None
1238+
1239+ # Bad hash.
1240+ with mock .patch .object (_imp , "check_hash_based_pycs" , "always" ):
1241+ pyc .write_bytes (magic + flags_hash + b"\x00 " * 8 + code )
1242+ assert _read_pyc (source , pyc , print ) is None
1243+
1244+ # check_hash_based_pycs == "always" with timestamp based pyc file.
1245+ with mock .patch .object (_imp , "check_hash_based_pycs" , "always" ):
1246+ pyc .write_bytes (magic + flags + mtime + size + code )
1247+ assert _read_pyc (source , pyc , print ) is None
1248+
11821249 def test_reload_is_same_and_reloads (self , pytester : Pytester ) -> None :
11831250 """Reloading a (collected) module after change picks up the change."""
11841251 pytester .makeini (
0 commit comments