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
3 changes: 2 additions & 1 deletion Lib/profiling/sampling/stack_collector.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ def export(self, filename):

lines.sort(key=lambda x: (-x[1], x[0]))

with open(filename, "w") as f:
with open(filename, "w",
encoding="utf-8", errors="surrogatepass") as f:
for stack, count in lines:
f.write(f"{stack} {count}\n")
print(f"Collapsed stack output written to {filename}")
Expand Down
11 changes: 3 additions & 8 deletions Lib/test/libregrtest/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,9 +330,6 @@ def get_build_info():
# Get most important configure and build options as a list of strings.
# Example: ['debug', 'ASAN+MSAN'] or ['release', 'LTO+PGO'].

config_args = sysconfig.get_config_var('CONFIG_ARGS') or ''
cflags = sysconfig.get_config_var('PY_CFLAGS') or ''
cflags += ' ' + (sysconfig.get_config_var('PY_CFLAGS_NODIST') or '')
ldflags_nodist = sysconfig.get_config_var('PY_LDFLAGS_NODIST') or ''

build = []
Expand All @@ -351,18 +348,16 @@ def get_build_info():
free_threading = f"{free_threading} GIL={int(PYTHON_GIL)}"
build.append(free_threading)

if hasattr(sys, 'gettotalrefcount'):
if support.Py_DEBUG:
# --with-pydebug
build.append('debug')

if '-DNDEBUG' in cflags:
if not support.built_with_c_assertions():
build.append('without_assert')
else:
build.append('release')

if '--with-assertions' in config_args:
build.append('with_assert')
elif '-DNDEBUG' not in cflags:
if support.built_with_c_assertions():
build.append('with_assert')

# --enable-experimental-jit
Expand Down
10 changes: 2 additions & 8 deletions Lib/test/pythoninfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -611,14 +611,6 @@ def collect_sysconfig(info_add):
value = normalize_text(value)
info_add('sysconfig[%s]' % name, value)

PY_CFLAGS = sysconfig.get_config_var('PY_CFLAGS')
NDEBUG = (PY_CFLAGS and '-DNDEBUG' in PY_CFLAGS)
if NDEBUG:
text = 'ignore assertions (macro defined)'
else:
text= 'build assertions (macro not defined)'
info_add('build.NDEBUG',text)

for name in (
'WITH_DOC_STRINGS',
'WITH_DTRACE',
Expand Down Expand Up @@ -844,6 +836,8 @@ def collect_support(info_add):
support.check_sanitizer(memory=True))
info_add('support.check_sanitizer(ub=True)',
support.check_sanitizer(ub=True))
info_add('support.built_with_c_assertions',
support.built_with_c_assertions())


def collect_support_os_helper(info_add):
Expand Down
17 changes: 16 additions & 1 deletion Lib/test/support/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
"run_no_yield_async_fn", "run_yielding_async_fn", "async_yield",
"reset_code", "on_github_actions",
"requires_root_user", "requires_non_root_user",
"skip_if_double_rounding",
"skip_if_double_rounding", "built_with_c_assertions",
]


Expand Down Expand Up @@ -3526,3 +3526,18 @@ def check_immutable_type(testcase, type):
else:
flags = type_getflags(type)
testcase.assertTrue(flags & Py_TPFLAGS_IMMUTABLETYPE)


def built_with_c_assertions():
"""Check if Python was built with C assertions (assert())."""

if MS_WINDOWS:
# On Windows, rely on the Py_DEBUG macro to check for assertions
return Py_DEBUG

# Check if the NDEBUG macro is defined in C compiler flags
PY_CFLAGS = (sysconfig.get_config_var('PY_CFLAGS') or '')
if '-DNDEBUG' in PY_CFLAGS:
return False

return True
12 changes: 2 additions & 10 deletions Lib/test/test_gc.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

import gc
import sys
import sysconfig
import textwrap
import threading
import time
Expand Down Expand Up @@ -79,13 +78,6 @@ def __init__(self, partner=None):
def __tp_del__(self):
pass

if sysconfig.get_config_vars().get('PY_CFLAGS', ''):
BUILD_WITH_NDEBUG = ('-DNDEBUG' in sysconfig.get_config_vars()['PY_CFLAGS'])
else:
# Usually, sys.gettotalrefcount() is only present if Python has been
# compiled in debug mode. If it's missing, expect that Python has
# been released in release mode: with NDEBUG defined.
BUILD_WITH_NDEBUG = (not hasattr(sys, 'gettotalrefcount'))

### Tests
###############################################################################
Expand Down Expand Up @@ -1422,8 +1414,8 @@ def test_collect_garbage(self):


@requires_subprocess()
@unittest.skipIf(BUILD_WITH_NDEBUG,
'built with -NDEBUG')
@unittest.skipIf(not support.built_with_c_assertions(),
'built without C assertions')
def test_refcount_errors(self):
self.preclean()
# Verify the "handling" of objects with broken refcounts
Expand Down
22 changes: 22 additions & 0 deletions Lib/test/test_profiling/test_sampling_profiler/test_collectors.py
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,28 @@ def test_collapsed_stack_collector_export(self):
self.assertIn(stack1_expected, lines)
self.assertIn(stack2_expected, lines)

def test_collapsed_stack_collector_export_non_ascii_names(self):
# gh-156810: frame names are written verbatim, so the output must be
# opened with an encoding that can represent non-ASCII and
# surrogate-escaped (undecodable-path) names.
collapsed_out = tempfile.NamedTemporaryFile(delete=False)
self.addCleanup(close_and_unlink, collapsed_out)

collector = CollapsedStackCollector(1000)
frame = MockFrameInfo("/tmp/ba\udc80d.py", 5, "计算")
collector.collect([
MockInterpreterInfo(0, [MockThreadInfo(1, [frame])])
])

with captured_stdout(), captured_stderr():
collector.export(collapsed_out.name)

with open(collapsed_out.name, encoding="utf-8",
errors="surrogatepass") as f:
content = f.read()
self.assertIn("计算", content)
self.assertIn("ba\udc80d.py", content)

def test_flamegraph_collector_basic(self):
"""Test basic FlamegraphCollector functionality."""
collector = FlamegraphCollector(1000)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Fix a :exc:`UnicodeEncodeError` crash in the sampling profiler's
collapsed-stack export (``--collapsed``) when a sampled frame's function or
file name contains non-ASCII or surrogate-escaped characters. The output file
is now written as UTF-8.
19 changes: 11 additions & 8 deletions Modules/_io/textio.c
Original file line number Diff line number Diff line change
Expand Up @@ -1671,17 +1671,17 @@ _textiowrapper_writeflush(textio *self)
}
else {
assert(PyList_Check(pending));
b = PyBytes_FromStringAndSize(NULL, self->pending_bytes_count);
if (b == NULL) {
PyBytesWriter *writer = PyBytesWriter_Create(self->pending_bytes_count);
if (writer == NULL) {
return -1;
}

char *buf = PyBytes_AsString(b);
char *buf = PyBytesWriter_GetData(writer);
Py_ssize_t pos = 0;

for (Py_ssize_t i = 0; i < PyList_GET_SIZE(pending); i++) {
PyObject *obj = PyList_GET_ITEM(pending, i);
char *src;
const char *src;
Py_ssize_t len;
if (PyUnicode_Check(obj)) {
assert(PyUnicode_IS_ASCII(obj));
Expand All @@ -1690,15 +1690,18 @@ _textiowrapper_writeflush(textio *self)
}
else {
assert(PyBytes_Check(obj));
if (PyBytes_AsStringAndSize(obj, &src, &len) < 0) {
Py_DECREF(b);
return -1;
}
src = PyBytes_AS_STRING(obj);
len = PyBytes_GET_SIZE(obj);
}
memcpy(buf + pos, src, len);
pos += len;
}
assert(pos == self->pending_bytes_count);

b = PyBytesWriter_Finish(writer);
if (b == NULL) {
return -1;
}
}

self->pending_bytes_count = 0;
Expand Down
Loading