Files
mongo/buildscripts/tests/resmoke_end2end/test_resmoke_js_debugger.py
Steve McClure a0c8c2e8fe SERVER-119407: Add resmoke --shellJSDebugMode flag to enable JS debugger handling (#48095)
GitOrigin-RevId: 0191e52d17ebcc4c42781e53cfec43124d4e8570
2026-02-14 00:18:25 +00:00

301 lines
11 KiB
Python

"""Test resmoke's JavaScript debugger functionality."""
import io
import logging
import os
import re
import subprocess
import sys
import unittest
from shutil import rmtree
import pexpect
class _ResmokeSelftest(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.test_dir = os.path.normpath("/data/db/selftest")
def setUp(self):
self.logger = logging.getLogger(self._testMethodName)
self.logger.setLevel(logging.DEBUG)
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(logging.Formatter(fmt="%(message)s"))
self.logger.addHandler(handler)
self.logger.info("Cleaning temp directory %s", self.test_dir)
rmtree(self.test_dir, ignore_errors=True)
os.makedirs(self.test_dir, mode=0o755, exist_ok=True)
def execute_resmoke(resmoke_args, subcommand="run"):
return subprocess.run(
[sys.executable, "buildscripts/resmoke.py", subcommand] + resmoke_args,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
class TestJSDebugger(_ResmokeSelftest):
"""Test suite for JavaScript debugger functionality."""
def test_debugger_without_flag(self):
"""Test that debugger statements are no-ops when --shellJSDebugMode is not set."""
# Without the flag, debugger statements should be no-ops, so this test passes
resmoke_args = [
"--suites=buildscripts/tests/resmoke_end2end/suites/resmoke_debugger_nodb.yml",
"buildscripts/tests/resmoke_end2end/testfiles/debugger/debugger_statement.js",
]
result = execute_resmoke(resmoke_args)
self.assertEqual(result.returncode, 0)
def test_debugger_waits_for_input(self):
"""Test that debugger pauses execution when hitting a debugger statement."""
# This test verifies the debugger is activated by confirming it pauses execution
# Note: The mongo shell's debugger requires /dev/tty for interactive input,
# so we can only verify that it pauses, not that it responds to commands in automation
resmoke_cmd = [
sys.executable,
"buildscripts/resmoke.py",
"run",
f"--dbpathPrefix={self.test_dir}",
"--shellJSDebugMode",
"--suites=buildscripts/tests/resmoke_end2end/suites/resmoke_debugger_nodb.yml",
"buildscripts/tests/resmoke_end2end/testfiles/debugger/debugger_statement.js",
]
process = subprocess.Popen(
resmoke_cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
# Wait for a short time - the debugger should pause and not complete
try:
stdout, _ = process.communicate(timeout=10)
# If we get here without timeout, the debugger didn't activate properly
self.logger.error("Test output:\n%s", stdout)
self.fail("Expected debugger to pause execution, but test completed")
except subprocess.TimeoutExpired:
# This is the expected behavior - debugger is waiting for input
process.kill()
stdout, _ = process.communicate()
self.logger.info("Debugger correctly paused execution. Output:\n%s", stdout)
# Verify the debugger prompt appeared
self.assertIn("JSDEBUG>", stdout)
self.assertIn("paused in 'debugger' statement", stdout)
class TestJSDebuggerInteractive(_ResmokeSelftest):
"""Test suite for interactive JavaScript debugger functionality."""
def run_debugger_test(self, test_file, commands, timeout=30):
"""Helper to run debugger and execute commands.
Args:
test_file: Path to the JS test file
commands: List of (command, expected_output) tuples
timeout: Maximum time to wait for each command
Returns:
Full output from the session
"""
resmoke_cmd = " ".join(
[
sys.executable,
"buildscripts/resmoke.py",
"run",
f"--dbpathPrefix={self.test_dir}",
"--shellJSDebugMode",
"--suites=buildscripts/tests/resmoke_end2end/suites/resmoke_debugger_nodb.yml",
test_file,
]
)
child = pexpect.spawn(resmoke_cmd, timeout=timeout, encoding="utf-8")
# Use StringIO to capture all output
output_buffer = io.StringIO()
child.logfile = output_buffer
try:
# Wait for initial debugger prompts - there are usually two in the initial pause
child.expect("JSDEBUG>", timeout=20)
self.logger.info("First debugger prompt detected")
# Wait for the "Type 'dbcont' to continue" prompt
child.expect("JSDEBUG>", timeout=5)
self.logger.info("Second debugger prompt detected, ready for commands")
# Execute each command
for cmd, expected in commands:
self.logger.info(f"Sending command: {cmd}")
child.sendline(cmd)
# Wait for the next prompt to ensure command completed
# and capture what comes before it
try:
child.expect("JSDEBUG>", timeout=5)
command_output = child.before if hasattr(child, "before") else ""
except pexpect.TIMEOUT:
command_output = ""
# Also check what's in the full buffer
full_buffer = output_buffer.getvalue()
self.logger.info(
f"Full buffer so far ({len(full_buffer)} chars, last 1000):\n{full_buffer[-1000:]}"
)
if expected:
# Log what we're looking for and what we got back
self.logger.info(f"Expect to find in output: {expected}")
self.logger.info(
f"child.before output (last 1000 chars): {command_output[-1000:] if command_output else 'empty'}"
)
# Check if expected is in the command output or full buffer
search_text = full_buffer # Use full buffer instead of just child.before
if expected not in search_text:
# Try to match as regex
if not re.search(expected, search_text):
self.logger.error(f"Pattern '{expected}' not found")
self.logger.error(f"Full buffer:\n{full_buffer}")
raise AssertionError(f"Pattern '{expected}' not found in output")
self.logger.info(f"Successfully found expected pattern: {expected}")
# Wait for process to finish or timeout
try:
child.expect(pexpect.EOF, timeout=10)
except pexpect.TIMEOUT:
pass
# Return all collected output
full_output = output_buffer.getvalue()
self.logger.info(f"Full output length: {len(full_output)} chars")
return full_output
finally:
child.close(force=True)
output_buffer.close()
def test_debugger_continue_with_dbcont(self):
"""Test that dbcont command continues execution."""
commands = [
("dbcont", None), # Continue execution
]
self.run_debugger_test(
"buildscripts/tests/resmoke_end2end/testfiles/debugger/debugger_statement.js",
commands,
)
# If we get here without timeout, the test passed
pass
def test_debugger_inspect_variables(self):
"""Test inspecting variables at debugger breakpoint."""
commands = [
# Check variable outputs
("x", "42"),
("y", r'[ "a", 15, [ 1, 2, 3 ] ]'),
("z", r'{ "foo" : [ 3, "bar" ] }'),
# ("z", r'[ 3, "bar" ] }'),
("dbcont", None), # Continue
]
output = self.run_debugger_test(
"buildscripts/tests/resmoke_end2end/testfiles/debugger/debugger_statement.js",
commands,
)
self.assertIn("42", output)
def test_debugger_undefined_variable(self):
"""Test that accessing undefined variables shows ReferenceError."""
commands = [
("q", "ReferenceError"), # q is not defined
("dbcont", None),
]
output = self.run_debugger_test(
"buildscripts/tests/resmoke_end2end/testfiles/debugger/debugger_statement.js",
commands,
)
self.assertIn("ReferenceError", output)
def test_debugger_syntax_error(self):
"""Test that syntax errors are caught in debugger."""
commands = [
("]]", "SyntaxError"), # Invalid syntax
("dbcont", None),
]
output = self.run_debugger_test(
"buildscripts/tests/resmoke_end2end/testfiles/debugger/debugger_statement.js",
commands,
)
self.assertIn("SyntaxError", output)
def test_debugger_assertion_error(self):
"""Test that assertion failures are shown in debugger."""
commands = [
("assert.eq(1, 2)", None), # Should fail - don't expect specific text
("dbcont", None),
]
output = self.run_debugger_test(
"buildscripts/tests/resmoke_end2end/testfiles/debugger/debugger_statement.js",
commands,
)
# The assertion should fail with an error message containing relevant keywords
# The mongo shell may show different error formats
self.assertTrue(
("1" in output and "2" in output) # Numbers from assertion
or "Error" in output
or "assert" in output.lower(),
f"Expected assertion error output, got: {output[:500]}",
)
def test_debugger_modify_variables(self):
"""Test modifying variables at debugger breakpoint."""
# Create a test file that expects modified variables
commands = [
("x", "42"), # Check original value
("x = 7", None), # Modify x
("y[1]", "15"), # Check array element
("y[1] = 99", None), # Modify array
("dbcont", None), # Continue - should pass assertions
]
output = self.run_debugger_test(
"buildscripts/tests/resmoke_end2end/testfiles/debugger/simple_debugger.js",
commands,
)
# The test should pass because we modified the variables
self.assertIn("Test Passed", output)
def test_debugger_complex_expressions(self):
"""Test evaluating complex expressions in debugger."""
commands = [
("x + 10", "52"), # Math expression
("typeof x", "number"), # Type check
("[1,2,3].length", "3"), # Array operations
("dbcont", None),
]
output = self.run_debugger_test(
"buildscripts/tests/resmoke_end2end/testfiles/debugger/debugger_statement.js",
commands,
)
self.assertIn("52", output)
self.assertIn("number", output)