diff --git a/Python/Product/Common/Parsing/PythonLanguageVersion.cs b/Python/Product/Common/Parsing/PythonLanguageVersion.cs index db3662ed19..0c9ca02898 100644 --- a/Python/Product/Common/Parsing/PythonLanguageVersion.cs +++ b/Python/Product/Common/Parsing/PythonLanguageVersion.cs @@ -46,6 +46,7 @@ public enum PythonLanguageVersion { V312 = 0x030c, V313 = 0x030d, V314 = 0x030e, // Added for Python 3.14 support + V315 = 0x030f, // Added for Python 3.15 support } public static class PythonLanguageVersionExtensions { diff --git a/Python/Product/Debugger.Concord/ExpressionEvaluator.cs b/Python/Product/Debugger.Concord/ExpressionEvaluator.cs index 8748663e2c..a1286dc249 100644 --- a/Python/Product/Debugger.Concord/ExpressionEvaluator.cs +++ b/Python/Product/Debugger.Concord/ExpressionEvaluator.cs @@ -385,8 +385,8 @@ public void GetFrameLocals(DkmInspectionContext inspectionContext, DkmWorkList w // misaligned address and throws, which aborts the entire locals enumeration. Object // pointers are 8-byte aligned, so the mask is a no-op for untagged slots; it is 0 for // versions before 3.14, leaving their behavior byte-for-byte identical. - ulong localsPlusTagMask = - pythonFrame.Process.GetPythonRuntimeInfo().LanguageVersion >= PythonLanguageVersion.V314 ? 0x3ul : 0ul; + ulong localsPlusTagMask = PyInterpreterFrame.GetStackReferenceTagMask( + pythonFrame.Process.GetPythonRuntimeInfo().LanguageVersion); // Process cellvars and freevars first, because function arguments can appear in both cellvars and varnames if the argument is captured by a closure, // in which case we want to use the cellvar because the regular var slot will then be unused by Python (and in Python 3.4+, nulled out). diff --git a/Python/Product/Debugger.Concord/Proxies/PointerProxy.cs b/Python/Product/Debugger.Concord/Proxies/PointerProxy.cs index d7e69c0861..25559179ec 100644 --- a/Python/Product/Debugger.Concord/Proxies/PointerProxy.cs +++ b/Python/Product/Debugger.Concord/Proxies/PointerProxy.cs @@ -62,6 +62,10 @@ public PointerProxy ReinterpretCast(bool polymorphic = true) where TProxy : IDataProxy { return new PointerProxy(Process, Address, polymorphic); } + + internal static ulong RemoveTagBits(ulong value, ulong tagMask) { + return value & ~tagMask; + } } [DebuggerDisplay("& {TryRead()}")] @@ -122,7 +126,7 @@ public PointerProxy Raw { // The pointer value stored at Address, with any tag bits stripped. private ulong ReadTarget() { - return Raw.Read() & ~_tagMask; + return PointerProxy.RemoveTagBits(Raw.Read(), _tagMask); } public TProxy Read() { diff --git a/Python/Product/Debugger.Concord/Proxies/Structs/DebugOffsetsFieldProvider.cs b/Python/Product/Debugger.Concord/Proxies/Structs/DebugOffsetsFieldProvider.cs index 165b2b84ac..366bbc9b7e 100644 --- a/Python/Product/Debugger.Concord/Proxies/Structs/DebugOffsetsFieldProvider.cs +++ b/Python/Product/Debugger.Concord/Proxies/Structs/DebugOffsetsFieldProvider.cs @@ -33,11 +33,13 @@ internal interface IStructFieldOffsetProvider { } /// - /// Resolves a curated set of hot-path struct fields from CPython 3.14's _Py_DebugOffsets - /// table (see ) instead of the PDB. Coverage is intentionally limited - /// to the frame / code-object / thread-state fields that the mixed-mode stack walk and in-process - /// line-number computation depend on; every other field still resolves via the PDB. This is what - /// automatically tracks the free-threaded build's shifted object layouts. + /// Resolves a curated set of hot-path struct fields from CPython's _Py_DebugOffsets + /// table (see ) instead of the PDB, for the interpreter versions the + /// reader understands (currently 3.14 and 3.15). Coverage is intentionally limited to the frame / + /// code-object / thread-state fields that the mixed-mode stack walk and in-process line-number + /// computation depend on; every other field still resolves via the PDB. This is what + /// automatically tracks the free-threaded build's shifted object layouts. Every group/field this + /// map references exists in both the 3.14 and 3.15 layouts, so no per-version map is needed. /// /// The map keys are CPython struct/field names (matching 's field names /// and each proxy's StructName); the values are the corresponding diff --git a/Python/Product/Debugger.Concord/Proxies/Structs/PyDebugOffsets.cs b/Python/Product/Debugger.Concord/Proxies/Structs/PyDebugOffsets.cs index 9e0ea77c6a..818532589e 100644 --- a/Python/Product/Debugger.Concord/Proxies/Structs/PyDebugOffsets.cs +++ b/Python/Product/Debugger.Concord/Proxies/Structs/PyDebugOffsets.cs @@ -21,16 +21,19 @@ namespace Microsoft.PythonTools.Debugger.Concord.Proxies.Structs { /// /// Managed reader for CPython's self-describing _Py_DebugOffsets table - /// (CPython 3.14, Include/internal/pycore_debug_offsets.h). The table is placed + /// (Include/internal/pycore_debug_offsets.h). The table is placed /// at offset 0 of the exported _PyRuntime global so out-of-process debuggers can /// discover the byte offset of interesting fields (frames, code objects, thread state, /// builtin object layouts, ...) without relying on the interpreter's PDB. /// /// The table opens with an 8-byte "xdebugpy" cookie, a PY_VERSION_HEX /// version word and a free_threaded flag, followed by a flat run of little-endian - /// uint64_t offsets grouped by struct. This class only knows the 3.14 layout; the - /// header explicitly warns the layout is not stable across minor versions, so callers must - /// gate on before trusting anything but the cookie/version prefix. + /// uint64_t offsets grouped by struct. There is no per-group length in the blob, so the + /// byte position of every field is implied purely by the ordered list of groups/fields for that + /// interpreter version. CPython grows this table between minor versions (adding fields and whole + /// sub-structs), so this class keeps one ordered per version it understands + /// (3.14 and 3.15) and selects the matching one from the recorded version word. Callers should + /// gate on before trusting any offset. /// internal sealed class PyDebugOffsets { public const string CookieString = "xdebugpy"; @@ -39,9 +42,12 @@ internal sealed class PyDebugOffsets { private const int CookieSize = 8; private const int HeaderSize = CookieSize + sizeof(ulong) + sizeof(ulong); - // The 3.14 layout, mirroring pycore_debug_offsets.h field-for-field (every entry is - // a uint64 offset). Keeping the groups/fields in exact source order is what makes the - // byte positions correct by construction; do not reorder without matching the header. + // _Py_DebugOffsets is a flat run of grouped uint64 offsets with no per-group length, so the + // byte position of every field is implied by the ordered list of groups/fields. These lists + // must therefore mirror pycore_debug_offsets.h field-for-field, in exact source order, for + // each interpreter version we support. Do not reorder without matching the header. + + // Groups whose field set is identical across every layout we support (3.14 and 3.15). private static readonly string[] RuntimeStateFields = { "size", "finalizing", "interpreters_head", }; @@ -52,10 +58,6 @@ internal sealed class PyDebugOffsets { "gil_runtime_state_locked", "gil_runtime_state_holder", "code_object_generation", "tlbc_generation", }; - private static readonly string[] ThreadStateFields = { - "size", "prev", "next", "interp", "current_frame", "thread_id", - "native_thread_id", "datastack_chunk", "status", - }; private static readonly string[] InterpreterFrameFields = { "size", "previous", "executable", "instr_ptr", "localsplus", "owner", "stackpointer", "tlbc_index", @@ -68,9 +70,6 @@ internal sealed class PyDebugOffsets { private static readonly string[] PyObjectFields = { "size", "ob_type", }; - private static readonly string[] TypeObjectFields = { - "size", "tp_name", "tp_repr", "tp_flags", - }; private static readonly string[] TupleObjectFields = { "size", "ob_item", "ob_size", }; @@ -92,12 +91,6 @@ internal sealed class PyDebugOffsets { private static readonly string[] BytesObjectFields = { "size", "ob_size", "ob_sval", }; - private static readonly string[] UnicodeObjectFields = { - "size", "state", "length", "asciiobject_size", - }; - private static readonly string[] GcFields = { - "size", "collecting", - }; private static readonly string[] GenObjectFields = { "size", "gi_name", "gi_iframe", "gi_frame_state", }; @@ -109,15 +102,75 @@ internal sealed class PyDebugOffsets { "debugger_pending_call", "debugger_script_path", "debugger_script_path_size", }; - // Groups in the exact order they appear in _Py_DebugOffsets. - private static readonly Tuple[] Groups = { + // Groups whose field set changed between 3.14 and 3.15. + private static readonly string[] ThreadStateFields314 = { + "size", "prev", "next", "interp", "current_frame", "thread_id", + "native_thread_id", "datastack_chunk", "status", + }; + private static readonly string[] ThreadStateFields315 = { + "size", "prev", "next", "interp", "current_frame", "base_frame", + "last_profiled_frame", "last_profiled_frame_seq", "thread_id", + "native_thread_id", "datastack_chunk", "status", "holds_gil", + "gil_requested", "current_exception", "exc_state", + }; + private static readonly string[] TypeObjectFields314 = { + "size", "tp_name", "tp_repr", "tp_flags", + }; + private static readonly string[] TypeObjectFields315 = { + "size", "tp_name", "tp_repr", "tp_flags", "tp_basicsize", "tp_dictoffset", + }; + private static readonly string[] UnicodeObjectFields314 = { + "size", "state", "length", "asciiobject_size", + }; + private static readonly string[] UnicodeObjectFields315 = { + "size", "state", "length", "asciiobject_size", "compactunicodeobject_size", + }; + private static readonly string[] GcFields314 = { + "size", "collecting", + }; + private static readonly string[] GcFields315 = { + "size", "collecting", "frame", "generation_stats_size", "generation_stats", + }; + // Groups introduced in 3.15 (inserted mid-table relative to the 3.14 layout). + private static readonly string[] ErrStackItemFields315 = { + "exc_value", + }; + private static readonly string[] HeapTypeObjectFields315 = { + "size", "ht_cached_keys", + }; + + /// + /// One interpreter version's ordered view of _Py_DebugOffsets: the groups in source + /// order plus the total number of table bytes they occupy (header + one uint64 per field). + /// The byte position of each field is implied by its position in this ordered list. + /// + private sealed class Layout { + public readonly int Major; + public readonly int Minor; + public readonly Tuple[] Groups; + public readonly int TableSize; + + public Layout(int major, int minor, Tuple[] groups) { + Major = major; + Minor = minor; + Groups = groups; + int fields = 0; + foreach (var group in groups) { + fields += group.Item2.Length; + } + TableSize = HeaderSize + fields * sizeof(ulong); + } + } + + // CPython 3.14 (Include/internal/pycore_debug_offsets.h @ branch 3.14). + private static readonly Layout Layout314 = new Layout(3, 14, new[] { Tuple.Create("runtime_state", RuntimeStateFields), Tuple.Create("interpreter_state", InterpreterStateFields), - Tuple.Create("thread_state", ThreadStateFields), + Tuple.Create("thread_state", ThreadStateFields314), Tuple.Create("interpreter_frame", InterpreterFrameFields), Tuple.Create("code_object", CodeObjectFields), Tuple.Create("pyobject", PyObjectFields), - Tuple.Create("type_object", TypeObjectFields), + Tuple.Create("type_object", TypeObjectFields314), Tuple.Create("tuple_object", TupleObjectFields), Tuple.Create("list_object", ListObjectFields), Tuple.Create("set_object", SetObjectFields), @@ -125,12 +178,65 @@ internal sealed class PyDebugOffsets { Tuple.Create("float_object", FloatObjectFields), Tuple.Create("long_object", LongObjectFields), Tuple.Create("bytes_object", BytesObjectFields), - Tuple.Create("unicode_object", UnicodeObjectFields), - Tuple.Create("gc", GcFields), + Tuple.Create("unicode_object", UnicodeObjectFields314), + Tuple.Create("gc", GcFields314), Tuple.Create("gen_object", GenObjectFields), Tuple.Create("llist_node", LListNodeFields), Tuple.Create("debugger_support", DebuggerSupportFields), - }; + }); + + // CPython 3.15 (Include/internal/pycore_debug_offsets.h @ branch 3.15). Relative to 3.14 the + // thread_state group gained 7 fields (3 inserted after current_frame, 4 appended), a new + // err_stackitem group was inserted before interpreter_frame, type_object gained 2 fields, a + // new heap_type_object group follows it, and unicode_object / gc gained fields. Every other + // group keeps its 3.14 order. + private static readonly Layout Layout315 = new Layout(3, 15, new[] { + Tuple.Create("runtime_state", RuntimeStateFields), + Tuple.Create("interpreter_state", InterpreterStateFields), + Tuple.Create("thread_state", ThreadStateFields315), + Tuple.Create("err_stackitem", ErrStackItemFields315), + Tuple.Create("interpreter_frame", InterpreterFrameFields), + Tuple.Create("code_object", CodeObjectFields), + Tuple.Create("pyobject", PyObjectFields), + Tuple.Create("type_object", TypeObjectFields315), + Tuple.Create("heap_type_object", HeapTypeObjectFields315), + Tuple.Create("tuple_object", TupleObjectFields), + Tuple.Create("list_object", ListObjectFields), + Tuple.Create("set_object", SetObjectFields), + Tuple.Create("dict_object", DictObjectFields), + Tuple.Create("float_object", FloatObjectFields), + Tuple.Create("long_object", LongObjectFields), + Tuple.Create("bytes_object", BytesObjectFields), + Tuple.Create("unicode_object", UnicodeObjectFields315), + Tuple.Create("gc", GcFields315), + Tuple.Create("gen_object", GenObjectFields), + Tuple.Create("llist_node", LListNodeFields), + Tuple.Create("debugger_support", DebuggerSupportFields), + }); + + private static readonly Layout[] KnownLayouts = { Layout314, Layout315 }; + + // Largest table across all known layouts. TryRead reads this many bytes before it knows the + // version, which is safe because the table is only the first member of the much larger + // _PyRuntime global; TryParse then consumes only the matching layout's TableSize. + private static readonly int MaxTableSize = GetMaxTableSize(); + + private static int GetMaxTableSize() { + int maxTableSize = 0; + foreach (var layout in KnownLayouts) { + maxTableSize = Math.Max(maxTableSize, layout.TableSize); + } + return maxTableSize; + } + + private static Layout GetLayout(int major, int minor) { + foreach (var layout in KnownLayouts) { + if (layout.Major == major && layout.Minor == minor) { + return layout; + } + } + return null; + } private readonly System.Collections.Generic.Dictionary _offsets; @@ -144,18 +250,26 @@ internal sealed class PyDebugOffsets { public int Minor => (int)((Version >> 16) & 0xFF); public int Micro => (int)((Version >> 8) & 0xFF); - /// True when this table describes a CPython 3.14 layout (the only layout this reader knows). + /// True when this table describes a CPython 3.14 layout. public bool Is314 => Major == 3 && Minor == 14; - /// Total number of bytes consumed by the parsed 3.14 table (header + all offset words). - public static int TableSize { - get { - int fields = 0; - foreach (var group in Groups) { - fields += group.Item2.Length; - } - return HeaderSize + fields * sizeof(ulong); - } + /// True when this table describes a CPython 3.15 layout. + public bool Is315 => Major == 3 && Minor == 15; + + /// + /// True when this reader knows the exact table layout for the recorded version and therefore + /// parsed every field at an authoritative position (currently CPython 3.14 and 3.15). Callers + /// that source offsets from the table must gate on this; when false, fall back to the PDB. + /// + public bool IsSupported => GetLayout(Major, Minor) != null; + + /// + /// Total number of table bytes (header + all offset words) for a known layout, or 0 if the + /// version is not one this reader understands. Exposed for tests. + /// + internal static int TableSizeFor(int major, int minor) { + var layout = GetLayout(major, minor); + return layout != null ? layout.TableSize : 0; } private PyDebugOffsets(ulong version, bool freeThreaded, System.Collections.Generic.Dictionary offsets) { @@ -174,8 +288,9 @@ public ulong Offset(string group, string field) { /// /// Attempts to parse a _Py_DebugOffsets table from raw bytes read out of the debuggee. - /// Validates the cookie and that the buffer is large enough for the full 3.14 layout, but does - /// not require a specific version so callers can inspect / . + /// Validates the cookie, reads the version word, selects the matching layout (3.14 or 3.15) and + /// verifies the buffer is large enough for it. Returns false for versions this reader does not + /// understand, so callers fall back to the PDB. /// public static bool TryParse(byte[] data, out PyDebugOffsets result, out string error) { result = null; @@ -196,15 +311,23 @@ public static bool TryParse(byte[] data, out PyDebugOffsets result, out string e ulong version = BitConverter.ToUInt64(data, CookieSize); ulong freeThreaded = BitConverter.ToUInt64(data, CookieSize + sizeof(ulong)); - int needed = TableSize; - if (data.Length < needed) { - error = "buffer too small for _Py_DebugOffsets 3.14 layout (need " + needed + " bytes, have " + data.Length + ")"; + int major = (int)((version >> 24) & 0xFF); + int minor = (int)((version >> 16) & 0xFF); + var layout = GetLayout(major, minor); + if (layout == null) { + error = "unsupported _Py_DebugOffsets version " + major + "." + minor; + return false; + } + + if (data.Length < layout.TableSize) { + error = "buffer too small for _Py_DebugOffsets " + major + "." + minor + + " layout (need " + layout.TableSize + " bytes, have " + data.Length + ")"; return false; } - var offsets = new System.Collections.Generic.Dictionary(needed / sizeof(ulong)); + var offsets = new System.Collections.Generic.Dictionary(layout.TableSize / sizeof(ulong)); int pos = HeaderSize; - foreach (var group in Groups) { + foreach (var group in layout.Groups) { foreach (var field in group.Item2) { offsets[group.Item1 + "." + field] = BitConverter.ToUInt64(data, pos); pos += sizeof(ulong); @@ -238,7 +361,10 @@ public static PyDebugOffsets TryRead(DkmProcess process, DkmNativeModuleInstance return null; } - var buffer = new byte[TableSize]; + // Read enough bytes for the largest layout we know; the version prefix then tells TryParse + // which layout to consume. The table is the first member of the much larger _PyRuntime + // global, so over-reading here is safe. + var buffer = new byte[MaxTableSize]; try { process.ReadMemory(address, DkmReadMemoryFlags.None, buffer); } catch (DkmException) { diff --git a/Python/Product/Debugger.Concord/Proxies/Structs/PyInterpreterFrame.cs b/Python/Product/Debugger.Concord/Proxies/Structs/PyInterpreterFrame.cs index f0ca8cba39..cf387e2419 100644 --- a/Python/Product/Debugger.Concord/Proxies/Structs/PyInterpreterFrame.cs +++ b/Python/Product/Debugger.Concord/Proxies/Structs/PyInterpreterFrame.cs @@ -54,6 +54,10 @@ internal class Fields { // the real PyObject* back. private const ulong StackRefTagMask = 0x3; + internal static ulong GetStackReferenceTagMask(PythonLanguageVersion version) { + return version >= PythonLanguageVersion.V314 ? StackRefTagMask : 0; + } + private readonly Fields _fields; public PyInterpreterFrame(DkmProcess process, ulong address) @@ -69,13 +73,14 @@ public PointerProxy f_code { } var executable = GetFieldProxy(_fields.f_executable); - if (Process.GetPythonRuntimeInfo().LanguageVersion >= PythonLanguageVersion.V314) { + var tagMask = GetStackReferenceTagMask(Process.GetPythonRuntimeInfo().LanguageVersion); + if (tagMask != 0) { // In 3.14, f_executable is a _PyStackRef rather than a plain PyObject*. Its two // low bits are a reference tag (Py_TAG_BITS, i.e. mask 0x3; set for deferred/ // immortal references such as frozen-module code objects), so strip them to // recover the PyCodeObject pointer. This mirrors CPython's own out-of-process // reader (CLEAR_PTR_TAG in _remote_debugging_module.c). - executable = executable.WithTagMask(StackRefTagMask); + executable = executable.WithTagMask(tagMask); } return executable; } diff --git a/Python/Product/Debugger.Concord/PythonRuntimeInfo.cs b/Python/Product/Debugger.Concord/PythonRuntimeInfo.cs index a21e342f51..2890536edd 100644 --- a/Python/Product/Debugger.Concord/PythonRuntimeInfo.cs +++ b/Python/Product/Debugger.Concord/PythonRuntimeInfo.cs @@ -24,7 +24,7 @@ namespace Microsoft.PythonTools.Debugger.Concord { internal class PythonDLLs { - private static readonly Regex pythonName = new Regex(@"^python(3\d+)(?:_d)?\.dll$"); + private static readonly Regex pythonName = new Regex(@"^python(3\d+)(t)?(?:_d)?\.dll$"); public static readonly string[] DebuggerHelperNames = { "Microsoft.PythonTools.Debugger.Helper.x86.dll", @@ -60,28 +60,38 @@ public DkmNativeModuleInstance Python { public DkmNativeModuleInstance CTypes { get; set; } public static PythonLanguageVersion GetPythonLanguageVersion(DkmNativeModuleInstance moduleInstance) { - var m = pythonName.Match(moduleInstance.Name); + return GetPythonLanguageVersion(moduleInstance.Name); + } + + internal static PythonLanguageVersion GetPythonLanguageVersion(string moduleName) { + var m = pythonName.Match(moduleName); if (!m.Success) { return PythonLanguageVersion.None; } var ver = m.Groups[1].Value; + PythonLanguageVersion version; switch (ver) { - case "27": return PythonLanguageVersion.V27; - case "33": return PythonLanguageVersion.V33; - case "34": return PythonLanguageVersion.V34; - case "35": return PythonLanguageVersion.V35; - case "36": return PythonLanguageVersion.V36; - case "37": return PythonLanguageVersion.V37; - case "38": return PythonLanguageVersion.V38; - case "39": return PythonLanguageVersion.V39; - case "310": return PythonLanguageVersion.V310; - case "311": return PythonLanguageVersion.V311; - case "312": return PythonLanguageVersion.V312; - case "313": return PythonLanguageVersion.V313; - case "314": return PythonLanguageVersion.V314; + case "27": version = PythonLanguageVersion.V27; break; + case "33": version = PythonLanguageVersion.V33; break; + case "34": version = PythonLanguageVersion.V34; break; + case "35": version = PythonLanguageVersion.V35; break; + case "36": version = PythonLanguageVersion.V36; break; + case "37": version = PythonLanguageVersion.V37; break; + case "38": version = PythonLanguageVersion.V38; break; + case "39": version = PythonLanguageVersion.V39; break; + case "310": version = PythonLanguageVersion.V310; break; + case "311": version = PythonLanguageVersion.V311; break; + case "312": version = PythonLanguageVersion.V312; break; + case "313": version = PythonLanguageVersion.V313; break; + case "314": version = PythonLanguageVersion.V314; break; + case "315": version = PythonLanguageVersion.V315; break; default: return PythonLanguageVersion.None; } + + return !m.Groups[2].Success || version >= PythonLanguageVersion.V313 + ? version + : PythonLanguageVersion.None; } } @@ -123,17 +133,18 @@ public Proxies.Structs.PyDebugOffsets DebugOffsets { /// /// Offset source that consults before falling back to the - /// interpreter PDB. Non-null only for CPython 3.14, where the _Py_DebugOffsets table - /// authoritatively describes the (potentially free-threaded-shifted) layout of the mixed-mode - /// hot-path structs. Older interpreters return null and resolve every field via the PDB exactly - /// as before, so this cannot regress them. + /// interpreter PDB. Non-null only for CPython versions whose _Py_DebugOffsets layout this + /// reader understands (3.14 and 3.15), where the table authoritatively describes the (potentially + /// free-threaded-shifted) layout of the mixed-mode hot-path structs. Older interpreters, and any + /// newer version this reader hasn't been taught yet, return null and resolve every field via the + /// PDB exactly as before, so this cannot regress them. /// public Proxies.Structs.IStructFieldOffsetProvider StructFieldOffsetProvider { get { if (!_offsetProviderProbed) { _offsetProviderProbed = true; var offsets = DebugOffsets; - if (offsets != null && offsets.Is314) { + if (offsets != null && offsets.IsSupported) { _offsetProvider = new Proxies.Structs.DebugOffsetsFieldProvider(offsets); } } diff --git a/Python/Tests/DebuggerTests/DebuggerTests.csproj b/Python/Tests/DebuggerTests/DebuggerTests.csproj index da9439228f..531dfc9434 100644 --- a/Python/Tests/DebuggerTests/DebuggerTests.csproj +++ b/Python/Tests/DebuggerTests/DebuggerTests.csproj @@ -72,6 +72,7 @@ + @@ -114,4 +115,4 @@ - \ No newline at end of file + diff --git a/Python/Tests/DebuggerTests/PyDebugOffsetsProviderTests.cs b/Python/Tests/DebuggerTests/PyDebugOffsetsProviderTests.cs index 6a44f0a1a9..2665a33e8f 100644 --- a/Python/Tests/DebuggerTests/PyDebugOffsetsProviderTests.cs +++ b/Python/Tests/DebuggerTests/PyDebugOffsetsProviderTests.cs @@ -21,10 +21,12 @@ namespace DebuggerTests { /// /// Verifies the hot-path offset provider () that lets - /// StructProxy source CPython 3.14 frame / code-object / thread-state field offsets from the - /// self-describing _Py_DebugOffsets table instead of the PDB. Everything is exercised against - /// the same real 3.14.6 vectors used by , mapping the CPython - /// struct/field names that StructProxy passes in to the values the table reports. + /// StructProxy source CPython frame / code-object / thread-state field offsets from the + /// self-describing _Py_DebugOffsets table instead of the PDB. The cases run against the real + /// 3.14.6 and 3.15.0rc1 vectors used by (standard and free-threaded), + /// plus a synthetic 3.15 blob that pins the mapping to the exact ordinals of the (grown) 3.15 layout. + /// Each case maps the CPython struct/field names that StructProxy passes in to the values the + /// table reports. /// [TestClass] public class DebugOffsetsFieldProviderTests { @@ -117,5 +119,88 @@ public void ReturnsFalse_ForUnmappedFieldsAndStructs() { Assert.IsFalse(provider.TryGetFieldOffset(null, "next", out offset)); Assert.IsFalse(provider.TryGetFieldOffset("_ts", null, out offset)); } + + [TestMethod, Priority(0)] + public void Standard315_MapsInterpreterFrameHotPath() { + var provider = Provider(PyDebugOffsetsTests.RawV315); + Assert.AreEqual(8L, Offset(provider, "_PyInterpreterFrame", "previous")); + Assert.AreEqual(0L, Offset(provider, "_PyInterpreterFrame", "f_executable")); + Assert.AreEqual(56L, Offset(provider, "_PyInterpreterFrame", "instr_ptr")); + Assert.AreEqual(80L, Offset(provider, "_PyInterpreterFrame", "localsplus")); + Assert.AreEqual(74L, Offset(provider, "_PyInterpreterFrame", "owner")); + } + + [TestMethod, Priority(0)] + public void Standard315_MapsCodeObjectHotPath() { + var provider = Provider(PyDebugOffsetsTests.RawV315); + Assert.AreEqual(112L, Offset(provider, "PyCodeObject", "co_filename")); + Assert.AreEqual(120L, Offset(provider, "PyCodeObject", "co_name")); + Assert.AreEqual(68L, Offset(provider, "PyCodeObject", "co_firstlineno")); + Assert.AreEqual(96L, Offset(provider, "PyCodeObject", "co_localsplusnames")); + Assert.AreEqual(104L, Offset(provider, "PyCodeObject", "co_localspluskinds")); + Assert.AreEqual(208L, Offset(provider, "PyCodeObject", "co_code_adaptive")); + Assert.AreEqual(136L, Offset(provider, "PyCodeObject", "co_linetable")); + } + + [TestMethod, Priority(0)] + public void Standard315_MapsThreadStateHotPath() { + var provider = Provider(PyDebugOffsetsTests.RawV315); + Assert.AreEqual(8L, Offset(provider, "_ts", "next")); + Assert.AreEqual(16L, Offset(provider, "_ts", "interp")); + Assert.AreEqual(176L, Offset(provider, "_ts", "thread_id")); + Assert.AreEqual(72L, Offset(provider, "_ts", "current_frame")); + } + + [TestMethod, Priority(0)] + public void FreeThreaded315_TracksShiftedLayout() { + var standard = Provider(PyDebugOffsetsTests.RawV315); + var freeThreaded = Provider(PyDebugOffsetsTests.RawV315T); + + // Same story as 3.14: the free-threaded build shifts these code-object fields and the + // provider surfaces the shifted offsets with no code change (co_linetable 136->152, + // co_firstlineno 68->84). + Assert.AreEqual(136L, Offset(standard, "PyCodeObject", "co_linetable")); + Assert.AreEqual(152L, Offset(freeThreaded, "PyCodeObject", "co_linetable")); + Assert.AreEqual(68L, Offset(standard, "PyCodeObject", "co_firstlineno")); + Assert.AreEqual(84L, Offset(freeThreaded, "PyCodeObject", "co_firstlineno")); + + // Fields that don't move stay put across builds. + Assert.AreEqual(56L, Offset(freeThreaded, "_PyInterpreterFrame", "instr_ptr")); + Assert.AreEqual(72L, Offset(freeThreaded, "_ts", "current_frame")); + } + + // Builds a provider over a synthetic 3.15 table where field i holds value i, so the hot-path + // mappings must resolve to each field's ordinal in the 3.15 layout (see PyDebugOffsetsTests for + // how the ordinals are derived from the CPython 3.15 header). + private static DebugOffsetsFieldProvider Synthetic315Provider() { + PyDebugOffsets offsets; + string error; + Assert.IsTrue(PyDebugOffsets.TryParse(PyDebugOffsetsTests.BuildSynthetic315(), out offsets, out error), error); + Assert.IsTrue(offsets.Is315); + return new DebugOffsetsFieldProvider(offsets); + } + + [TestMethod, Priority(0)] + public void Synthetic315_MapsHotPathAgainstGrownLayout() { + var provider = Synthetic315Provider(); + + // interpreter_frame hot path (ordinals shifted by the 3.15 insertions). + Assert.AreEqual(37L, Offset(provider, "_PyInterpreterFrame", "previous")); + Assert.AreEqual(38L, Offset(provider, "_PyInterpreterFrame", "f_executable")); + Assert.AreEqual(39L, Offset(provider, "_PyInterpreterFrame", "instr_ptr")); + Assert.AreEqual(40L, Offset(provider, "_PyInterpreterFrame", "localsplus")); + Assert.AreEqual(41L, Offset(provider, "_PyInterpreterFrame", "owner")); + + // code_object hot path. + Assert.AreEqual(48L, Offset(provider, "PyCodeObject", "co_linetable")); + Assert.AreEqual(49L, Offset(provider, "PyCodeObject", "co_firstlineno")); + Assert.AreEqual(53L, Offset(provider, "PyCodeObject", "co_code_adaptive")); + + // thread_state hot path (current_frame keeps ordinal 23; interp/next precede it). + Assert.AreEqual(21L, Offset(provider, "_ts", "next")); + Assert.AreEqual(22L, Offset(provider, "_ts", "interp")); + Assert.AreEqual(23L, Offset(provider, "_ts", "current_frame")); + Assert.AreEqual(27L, Offset(provider, "_ts", "thread_id")); + } } } diff --git a/Python/Tests/DebuggerTests/PyDebugOffsetsTests.cs b/Python/Tests/DebuggerTests/PyDebugOffsetsTests.cs index 505008506b..fa7c01bb98 100644 --- a/Python/Tests/DebuggerTests/PyDebugOffsetsTests.cs +++ b/Python/Tests/DebuggerTests/PyDebugOffsetsTests.cs @@ -62,6 +62,42 @@ public class PyDebugOffsetsTests { "00000000b000000000000000280000000000000058000000000000005300000000000000000000000000000008000000000000001800000000000000" + "2801000000000000f81e000000000000000000000000000004000000000000000002000000000000"; + // Recorded _Py_DebugOffsets bytes from CPython 3.15.0rc1 (standard, GIL-enabled build). + internal const string RawV315 = + "7864656275677079c1000f03000000000000000000000000a0440500000000009003000000000000a803000000000000f872030000000000681c0000" + + "00000000601c000000000000a01c000000000000b01c000000000000e01c000000000000a01d000000000000901d000000000000981d000000000000" + + "1000000000000000281e0000000000000000000000000000381e000000000000301e000000000000601e000000000000000000000000000070030000" + + "000000000000000000000000080000000000000010000000000000004800000000000000500000000000000058000000000000006000000000000000" + + "b000000000000000b400000000000000f800000000000000200000000000000024000000000000002800000000000000880000000000000018010000" + + "000000000000000000000000580000000000000008000000000000000000000000000000380000000000000050000000000000004a00000000000000" + + "40000000000000000000000000000000d800000000000000700000000000000078000000000000008000000000000000880000000000000044000000" + + "00000000340000000000000060000000000000006800000000000000d000000000000000000000000000000010000000000000000800000000000000" + + "a80100000000000018000000000000005800000000000000a80000000000000020000000000000002001000000000000b00300000000000078030000" + + "00000000280000000000000020000000000000001000000000000000280000000000000018000000000000001000000000000000c800000000000000" + + "180000000000000028000000000000002000000000000000300000000000000020000000000000002800000000000000180000000000000010000000" + + "000000002000000000000000100000000000000018000000000000002800000000000000100000000000000020000000000000004000000000000000" + + "2000000000000000100000000000000028000000000000003800000000000000b0000000000000007000000000000000780000000000000058040000" + + "000000006800000000000000a00000000000000018000000000000004800000000000000430000000000000000000000000000000800000000000000" + + "18000000000000005001000000000000b01e000000000000000000000000000004000000000000000002000000000000"; + + // Recorded _Py_DebugOffsets bytes from CPython 3.15.0rc1 free-threaded build (python3.15t). + internal const string RawV315T = + "7864656275677079c1000f03000000000100000000000000002d0600000000009003000000000000a80300000000000080c4030000000000681c0000" + + "00000000601c000000000000a01c000000000000b01c000000000000e01c000000000000a01d000000000000901d000000000000981d000000000000" + + "1000000000000000301e0000000000000000000000000000401e000000000000381e000000000000681e0000000000001c4400000000000070030000" + + "000000000000000000000000080000000000000010000000000000004800000000000000500000000000000058000000000000006000000000000000" + + "b000000000000000b400000000000000f800000000000000200000000000000024000000000000002800000000000000880000000000000018010000" + + "000000000000000000000000580000000000000008000000000000000000000000000000380000000000000050000000000000004e00000000000000" + + "40000000000000004800000000000000f000000000000000800000000000000088000000000000009000000000000000980000000000000054000000" + + "00000000440000000000000070000000000000007800000000000000e800000000000000e00000000000000020000000000000001800000000000000" + + "b80100000000000028000000000000006800000000000000b80000000000000030000000000000003001000000000000c80300000000000088030000" + + "00000000380000000000000030000000000000002000000000000000380000000000000028000000000000002000000000000000d800000000000000" + + "280000000000000038000000000000003000000000000000400000000000000030000000000000003800000000000000280000000000000020000000" + + "000000003000000000000000200000000000000028000000000000003800000000000000200000000000000030000000000000005000000000000000" + + "3000000000000000200000000000000038000000000000004800000000000000b00000000000000070000000000000007800000000000000d8000000" + + "000000006800000000000000b00000000000000028000000000000005800000000000000530000000000000000000000000000000800000000000000" + + "18000000000000005001000000000000b81e000000000000000000000000000004000000000000000002000000000000"; + // Full expected offsets for the standard 3.14.6 build (every field in the table). private static readonly Tuple[] ExpectedV314 = { T("runtime_state", "size", 315600), T("runtime_state", "finalizing", 784), T("runtime_state", "interpreters_head", 808), @@ -99,6 +135,31 @@ public class PyDebugOffsetsTests { T("debugger_support", "debugger_script_path", 4), T("debugger_support", "debugger_script_path_size", 512), }; + // Full expected offsets for the standard 3.15.0rc1 build (every field in the table). + private static readonly Tuple[] ExpectedV315 = { + T("runtime_state", "size", 345248), T("runtime_state", "finalizing", 912), T("runtime_state", "interpreters_head", 936), + T("interpreter_state", "size", 226040), T("interpreter_state", "id", 7272), T("interpreter_state", "next", 7264), T("interpreter_state", "threads_head", 7328), T("interpreter_state", "threads_main", 7344), T("interpreter_state", "gc", 7392), T("interpreter_state", "imports_modules", 7584), T("interpreter_state", "sysdict", 7568), T("interpreter_state", "builtins", 7576), T("interpreter_state", "ceval_gil", 16), T("interpreter_state", "gil_runtime_state", 7720), T("interpreter_state", "gil_runtime_state_enabled", 0), T("interpreter_state", "gil_runtime_state_locked", 7736), T("interpreter_state", "gil_runtime_state_holder", 7728), T("interpreter_state", "code_object_generation", 7776), T("interpreter_state", "tlbc_generation", 0), + T("thread_state", "size", 880), T("thread_state", "prev", 0), T("thread_state", "next", 8), T("thread_state", "interp", 16), T("thread_state", "current_frame", 72), T("thread_state", "base_frame", 80), T("thread_state", "last_profiled_frame", 88), T("thread_state", "last_profiled_frame_seq", 96), T("thread_state", "thread_id", 176), T("thread_state", "native_thread_id", 180), T("thread_state", "datastack_chunk", 248), T("thread_state", "status", 32), T("thread_state", "holds_gil", 36), T("thread_state", "gil_requested", 40), T("thread_state", "current_exception", 136), T("thread_state", "exc_state", 280), + T("err_stackitem", "exc_value", 0), + T("interpreter_frame", "size", 88), T("interpreter_frame", "previous", 8), T("interpreter_frame", "executable", 0), T("interpreter_frame", "instr_ptr", 56), T("interpreter_frame", "localsplus", 80), T("interpreter_frame", "owner", 74), T("interpreter_frame", "stackpointer", 64), T("interpreter_frame", "tlbc_index", 0), + T("code_object", "size", 216), T("code_object", "filename", 112), T("code_object", "name", 120), T("code_object", "qualname", 128), T("code_object", "linetable", 136), T("code_object", "firstlineno", 68), T("code_object", "argcount", 52), T("code_object", "localsplusnames", 96), T("code_object", "localspluskinds", 104), T("code_object", "co_code_adaptive", 208), T("code_object", "co_tlbc", 0), + T("pyobject", "size", 16), T("pyobject", "ob_type", 8), + T("type_object", "size", 424), T("type_object", "tp_name", 24), T("type_object", "tp_repr", 88), T("type_object", "tp_flags", 168), T("type_object", "tp_basicsize", 32), T("type_object", "tp_dictoffset", 288), + T("heap_type_object", "size", 944), T("heap_type_object", "ht_cached_keys", 888), + T("tuple_object", "size", 40), T("tuple_object", "ob_item", 32), T("tuple_object", "ob_size", 16), + T("list_object", "size", 40), T("list_object", "ob_item", 24), T("list_object", "ob_size", 16), + T("set_object", "size", 200), T("set_object", "used", 24), T("set_object", "table", 40), T("set_object", "mask", 32), + T("dict_object", "size", 48), T("dict_object", "ma_keys", 32), T("dict_object", "ma_values", 40), + T("float_object", "size", 24), T("float_object", "ob_fval", 16), + T("long_object", "size", 32), T("long_object", "lv_tag", 16), T("long_object", "ob_digit", 24), + T("bytes_object", "size", 40), T("bytes_object", "ob_size", 16), T("bytes_object", "ob_sval", 32), + T("unicode_object", "size", 64), T("unicode_object", "state", 32), T("unicode_object", "length", 16), T("unicode_object", "asciiobject_size", 40), T("unicode_object", "compactunicodeobject_size", 56), + T("gc", "size", 176), T("gc", "collecting", 112), T("gc", "frame", 120), T("gc", "generation_stats_size", 1112), T("gc", "generation_stats", 104), + T("gen_object", "size", 160), T("gen_object", "gi_name", 24), T("gen_object", "gi_iframe", 72), T("gen_object", "gi_frame_state", 67), + T("llist_node", "next", 0), T("llist_node", "prev", 8), + T("debugger_support", "eval_breaker", 24), T("debugger_support", "remote_debugger_support", 336), T("debugger_support", "remote_debugging_enabled", 7856), T("debugger_support", "debugger_pending_call", 0), T("debugger_support", "debugger_script_path", 4), T("debugger_support", "debugger_script_path_size", 512), + }; + private static Tuple T(string group, string field, ulong value) { return Tuple.Create(group, field, value); } @@ -189,7 +250,7 @@ public void TryParse_RejectsTooShortBuffer() { Assert.IsFalse(PyDebugOffsets.TryParse(null, out result, out error)); // A valid header but a buffer too short for the full 3.14 layout is rejected. - var truncated = new byte[PyDebugOffsets.TableSize - 1]; + var truncated = new byte[PyDebugOffsets.TableSizeFor(3, 14) - 1]; var full = FromHex(RawV314); Array.Copy(full, truncated, truncated.Length); Assert.IsFalse(PyDebugOffsets.TryParse(truncated, out result, out error)); @@ -197,8 +258,191 @@ public void TryParse_RejectsTooShortBuffer() { [TestMethod, Priority(0)] public void TableSize_MatchesRecordedVectorLength() { - Assert.AreEqual(FromHex(RawV314).Length, PyDebugOffsets.TableSize); - Assert.AreEqual(FromHex(RawV314T).Length, PyDebugOffsets.TableSize); + Assert.AreEqual(FromHex(RawV314).Length, PyDebugOffsets.TableSizeFor(3, 14)); + Assert.AreEqual(FromHex(RawV314T).Length, PyDebugOffsets.TableSizeFor(3, 14)); + Assert.AreEqual(FromHex(RawV315).Length, PyDebugOffsets.TableSizeFor(3, 15)); + Assert.AreEqual(FromHex(RawV315T).Length, PyDebugOffsets.TableSizeFor(3, 15)); + } + + // ------------------------------------------------------------------ + // CPython 3.15 layout. + // + // RawV315 / RawV315T are real _Py_DebugOffsets tables recorded in-process from CPython + // 3.15.0rc1 (standard and free-threaded builds). BuildSynthetic315 additionally builds a blob + // whose field i simply holds the value i (i = 0..N-1) so we can assert the reader places each + // group/field at the exact ordinal the CPython 3.15 header dictates + // (Include/internal/pycore_debug_offsets.h @ branch 3.15). The expected ordinals are derived + // independently from that header's group/field order, so the assertions are not circular with + // the reader's own field lists: + // runtime_state 3 [0..2] + // interpreter_state 16 [3..18] + // thread_state 16 [19..34] (+3 after current_frame, +4 appended vs 3.14) + // err_stackitem 1 [35] (new in 3.15) + // interpreter_frame 8 [36..43] + // code_object 11 [44..54] + // pyobject 2 [55..56] + // type_object 6 [57..62] (+tp_basicsize, +tp_dictoffset vs 3.14) + // heap_type_object 2 [63..64] (new in 3.15) + // ... remaining groups follow ... (108 fields total => 888 bytes) + internal static byte[] BuildSynthetic315() { + int size = PyDebugOffsets.TableSizeFor(3, 15); + Assert.AreEqual(888, size, "3.15 layout should be 24-byte header + 108 uint64 fields"); + var data = new byte[size]; + + for (int i = 0; i < 8; i++) { + data[i] = (byte)"xdebugpy"[i]; + } + // PY_VERSION_HEX for 3.15.1: major=3, minor=15, micro=1. + Array.Copy(BitConverter.GetBytes(0x030f0100UL), 0, data, 8, 8); + // free_threaded = 0 (standard build). + Array.Copy(BitConverter.GetBytes(0UL), 0, data, 16, 8); + + int fieldCount = (size - 24) / 8; + for (int i = 0; i < fieldCount; i++) { + Array.Copy(BitConverter.GetBytes((ulong)i), 0, data, 24 + i * 8, 8); + } + return data; + } + + [TestMethod, Priority(0)] + public void TryParse_ParsesStandard315Header() { + PyDebugOffsets result; + string error; + Assert.IsTrue(PyDebugOffsets.TryParse(FromHex(RawV315), out result, out error), error); + Assert.AreEqual(3, result.Major); + Assert.AreEqual(15, result.Minor); + Assert.AreEqual(0, result.Micro); + Assert.AreEqual(0x30f00c1UL, result.Version); + Assert.IsFalse(result.Is314); + Assert.IsTrue(result.Is315); + Assert.IsTrue(result.IsSupported); + Assert.IsFalse(result.FreeThreaded); + } + + [TestMethod, Priority(0)] + public void TryParse_ReadsEveryStandard315Offset() { + PyDebugOffsets result; + string error; + Assert.IsTrue(PyDebugOffsets.TryParse(FromHex(RawV315), out result, out error), error); + foreach (var expected in ExpectedV315) { + ulong actual = result.Offset(expected.Item1, expected.Item2); + Assert.AreEqual(expected.Item3, actual, expected.Item1 + "." + expected.Item2); + } + } + + [TestMethod, Priority(0)] + public void TryParse_ParsesFreeThreaded315() { + PyDebugOffsets result; + string error; + Assert.IsTrue(PyDebugOffsets.TryParse(FromHex(RawV315T), out result, out error), error); + Assert.IsTrue(result.Is315); + Assert.IsTrue(result.FreeThreaded); + + // instr_ptr / current_frame land at the same place as the standard 3.15 build... + Assert.AreEqual(56UL, result.Offset("interpreter_frame", "instr_ptr")); + Assert.AreEqual(72UL, result.Offset("thread_state", "current_frame")); + + // ...but the free-threaded build inserts extra fields, so object layouts shift. This is + // precisely why reading the table beats hard-coding offsets: linetable/firstlineno move + // within PyCodeObject and ob_type moves within PyObject (all +16 vs the standard build). + Assert.AreEqual(152UL, result.Offset("code_object", "linetable")); + Assert.AreEqual(84UL, result.Offset("code_object", "firstlineno")); + Assert.AreEqual(24UL, result.Offset("pyobject", "ob_type")); + + // The TLBC (thread-local bytecode) fields are only non-zero in the free-threaded build. + Assert.AreEqual(72UL, result.Offset("interpreter_frame", "tlbc_index")); + Assert.AreEqual(224UL, result.Offset("code_object", "co_tlbc")); + Assert.AreEqual(17436UL, result.Offset("interpreter_state", "tlbc_generation")); + } + + [TestMethod, Priority(0)] + public void TryParse_TlbcFieldsZeroInStandard315Build() { + PyDebugOffsets result; + string error; + Assert.IsTrue(PyDebugOffsets.TryParse(FromHex(RawV315), out result, out error), error); + Assert.AreEqual(0UL, result.Offset("interpreter_frame", "tlbc_index")); + Assert.AreEqual(0UL, result.Offset("code_object", "co_tlbc")); + Assert.AreEqual(0UL, result.Offset("interpreter_state", "tlbc_generation")); + } + + [TestMethod, Priority(0)] + public void TryParse_Parses315SyntheticHeader() { + PyDebugOffsets result; + string error; + Assert.IsTrue(PyDebugOffsets.TryParse(BuildSynthetic315(), out result, out error), error); + Assert.AreEqual(3, result.Major); + Assert.AreEqual(15, result.Minor); + Assert.AreEqual(1, result.Micro); + Assert.IsFalse(result.Is314); + Assert.IsTrue(result.Is315); + Assert.IsTrue(result.IsSupported); + Assert.IsFalse(result.FreeThreaded); + } + + [TestMethod, Priority(0)] + public void TryParse_Places315GroupsAtHeaderOrdinals() { + PyDebugOffsets result; + string error; + Assert.IsTrue(PyDebugOffsets.TryParse(BuildSynthetic315(), out result, out error), error); + + // thread_state gained fields in 3.15; current_frame stays at ordinal 23 but the appended + // fields (holds_gil..exc_state) land after the inserted profiling fields. + Assert.AreEqual(23UL, result.Offset("thread_state", "current_frame")); + Assert.AreEqual(24UL, result.Offset("thread_state", "base_frame")); + Assert.AreEqual(27UL, result.Offset("thread_state", "thread_id")); + Assert.AreEqual(34UL, result.Offset("thread_state", "exc_state")); + + // err_stackitem is a new group inserted between thread_state and interpreter_frame. + Assert.AreEqual(35UL, result.Offset("err_stackitem", "exc_value")); + + // interpreter_frame shifts by the 3.15 insertions but keeps its internal order. + Assert.AreEqual(38UL, result.Offset("interpreter_frame", "executable")); + Assert.AreEqual(39UL, result.Offset("interpreter_frame", "instr_ptr")); + Assert.AreEqual(40UL, result.Offset("interpreter_frame", "localsplus")); + Assert.AreEqual(41UL, result.Offset("interpreter_frame", "owner")); + + // code_object: the hot-path fields the in-process line decoder consumes. + Assert.AreEqual(48UL, result.Offset("code_object", "linetable")); + Assert.AreEqual(49UL, result.Offset("code_object", "firstlineno")); + Assert.AreEqual(53UL, result.Offset("code_object", "co_code_adaptive")); + + // type_object gained fields; heap_type_object is a new group right after it. + Assert.AreEqual(61UL, result.Offset("type_object", "tp_basicsize")); + Assert.AreEqual(62UL, result.Offset("type_object", "tp_dictoffset")); + Assert.AreEqual(64UL, result.Offset("heap_type_object", "ht_cached_keys")); + } + + [TestMethod, Priority(0)] + public void TryParse_Rejects315BufferTooShortForLayout() { + var full = BuildSynthetic315(); + var truncated = new byte[full.Length - 1]; + Array.Copy(full, truncated, truncated.Length); + PyDebugOffsets result; + string error; + Assert.IsFalse(PyDebugOffsets.TryParse(truncated, out result, out error)); + Assert.IsNull(result); + Assert.IsNotNull(error); + } + + [TestMethod, Priority(0)] + public void TryParse_RejectsUnknownVersion() { + // A well-formed header advertising an unknown version (e.g. a future 3.16) is rejected so + // callers fall back to the PDB rather than mis-parsing an unknown layout. + var data = BuildSynthetic315(); + Array.Copy(BitConverter.GetBytes(0x03100000UL), 0, data, 8, 8); // 3.16.0 + PyDebugOffsets result; + string error; + Assert.IsFalse(PyDebugOffsets.TryParse(data, out result, out error)); + Assert.IsNull(result); + Assert.IsNotNull(error); + } + + [TestMethod, Priority(0)] + public void TableSizeFor_UnknownVersionIsZero() { + Assert.AreEqual(0, PyDebugOffsets.TableSizeFor(3, 13)); + Assert.AreEqual(0, PyDebugOffsets.TableSizeFor(3, 16)); + Assert.AreEqual(760, PyDebugOffsets.TableSizeFor(3, 14)); + Assert.AreEqual(888, PyDebugOffsets.TableSizeFor(3, 15)); } } } diff --git a/Python/Tests/DebuggerTests/PythonRuntimeInfoTests.cs b/Python/Tests/DebuggerTests/PythonRuntimeInfoTests.cs new file mode 100644 index 0000000000..142fc127de --- /dev/null +++ b/Python/Tests/DebuggerTests/PythonRuntimeInfoTests.cs @@ -0,0 +1,67 @@ +// Python Tools for Visual Studio +// Copyright(c) Microsoft Corporation +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the License); you may not use +// this file except in compliance with the License. You may obtain a copy of the +// License at http://www.apache.org/licenses/LICENSE-2.0 +// +// THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS +// OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY +// IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +// MERCHANTABILITY OR NON-INFRINGEMENT. +// +// See the Apache Version 2.0 License for specific language governing +// permissions and limitations under the License. + +using Microsoft.PythonTools.Common.Parsing; +using Microsoft.PythonTools.Debugger.Concord; +using Microsoft.PythonTools.Debugger.Concord.Proxies; +using Microsoft.PythonTools.Debugger.Concord.Proxies.Structs; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace DebuggerTests { + [TestClass] + public class PythonRuntimeInfoTests { + [TestMethod, Priority(0)] + public void RecognizesPython315ModuleNames() { + var moduleNames = new[] { + "python315.dll", + "python315_d.dll", + "python315t.dll", + "python315t_d.dll", + }; + + foreach (var moduleName in moduleNames) { + var version = PythonDLLs.GetPythonLanguageVersion(moduleName); + Assert.AreEqual(PythonLanguageVersion.V315, version, moduleName); + } + } + + [TestMethod, Priority(0)] + public void StackReferenceHelpersClearTagBitsByVersion() { + const ulong taggedExecutable = 0x1234567BUL; + + Assert.AreEqual(taggedExecutable, + PointerProxy.RemoveTagBits(taggedExecutable, + PyInterpreterFrame.GetStackReferenceTagMask(PythonLanguageVersion.V313))); + Assert.AreEqual(0x12345678UL, + PointerProxy.RemoveTagBits(taggedExecutable, + PyInterpreterFrame.GetStackReferenceTagMask(PythonLanguageVersion.V314))); + Assert.AreEqual(0x12345678UL, + PointerProxy.RemoveTagBits(taggedExecutable, + PyInterpreterFrame.GetStackReferenceTagMask(PythonLanguageVersion.V315))); + } + + [TestMethod, Priority(0)] + public void RecognizesFreeThreadedModuleNamesSincePython313() { + Assert.AreEqual(PythonLanguageVersion.V313, PythonDLLs.GetPythonLanguageVersion("python313t.dll")); + Assert.AreEqual(PythonLanguageVersion.V313, PythonDLLs.GetPythonLanguageVersion("python313t_d.dll")); + Assert.AreEqual(PythonLanguageVersion.V314, PythonDLLs.GetPythonLanguageVersion("python314t.dll")); + Assert.AreEqual(PythonLanguageVersion.V314, PythonDLLs.GetPythonLanguageVersion("python314t_d.dll")); + Assert.AreEqual(PythonLanguageVersion.None, PythonDLLs.GetPythonLanguageVersion("python312t.dll")); + Assert.AreEqual(PythonLanguageVersion.None, + PythonDLLs.GetPythonLanguageVersion("python399999999999999999999999t.dll")); + } + } +}