diff --git a/lib/lsp-devtools/changes/130.fix.md b/lib/lsp-devtools/changes/130.fix.md new file mode 100644 index 0000000..2c810b2 --- /dev/null +++ b/lib/lsp-devtools/changes/130.fix.md @@ -0,0 +1 @@ +The `lsp-devtools record` command will now produce valid JSON when using the `--to-file` option without an explicitly provided format string. diff --git a/lib/lsp-devtools/lsp_devtools/record/__init__.py b/lib/lsp-devtools/lsp_devtools/record/__init__.py index 2f66f32..6d51f43 100644 --- a/lib/lsp-devtools/lsp_devtools/record/__init__.py +++ b/lib/lsp-devtools/lsp_devtools/record/__init__.py @@ -78,7 +78,7 @@ def log_rpc_message(ls: AgentServer, message: MessageText): parse_rpc_message(ls, message, logfn) -def setup_stdout_output(args) -> Console: +def setup_stdout_output(args, logger: logging.Logger) -> Console: """Log to stdout.""" console = Console(record=args.save_output is not None) @@ -90,7 +90,7 @@ def setup_stdout_output(args) -> Console: exclude_message_types=args.exclude_message_types, include_methods=args.include_methods, exclude_methods=args.exclude_methods, - formatter=args.format_message, + formatter=args.format_message or "{.|json}", ) ) @@ -98,7 +98,7 @@ def setup_stdout_output(args) -> Console: return console -def setup_file_output(args): +def setup_file_output(args, logger: logging.Logger): handler = logging.FileHandler(filename=str(args.to_file)) handler.setLevel(logging.INFO) handler.addFilter( @@ -108,14 +108,14 @@ def setup_file_output(args): exclude_message_types=args.exclude_message_types, include_methods=args.include_methods, exclude_methods=args.exclude_methods, - formatter=args.format_message, + formatter=args.format_message or "{.|json-compact}", ) ) logger.addHandler(handler) -def setup_sqlite_output(args): +def setup_sqlite_output(args, logger: logging.Logger): handler = SqlHandler(args.to_sqlite) handler.setLevel(logging.INFO) handler.addFilter( @@ -142,13 +142,13 @@ def start_recording(args, extra: List[str]): port = args.port if args.to_file: - setup_file_output(args) + setup_file_output(args, logger) elif args.to_sqlite: - setup_sqlite_output(args) + setup_sqlite_output(args, logger) else: - console = setup_stdout_output(args) + console = setup_stdout_output(args, logger) try: print(f"Waiting for connection on {host}:{port}...", end="\r", flush=True) @@ -266,7 +266,7 @@ def cli(commands: argparse._SubParsersAction): format.add_argument( "-f", "--format-message", - default="", + default=None, help=( "format messages according to given format string, " "see example commands above for syntax. " diff --git a/lib/lsp-devtools/tests/record/test_record.py b/lib/lsp-devtools/tests/record/test_record.py new file mode 100644 index 0000000..cb6d862 --- /dev/null +++ b/lib/lsp-devtools/tests/record/test_record.py @@ -0,0 +1,123 @@ +from __future__ import annotations + +import argparse +import logging +import typing + +import pytest + +from lsp_devtools.record import cli +from lsp_devtools.record import setup_file_output + +if typing.TYPE_CHECKING: + import pathlib + from typing import Any + from typing import Dict + from typing import List + + +@pytest.fixture(scope="module") +def record(): + """Return a cli parser for the record command.""" + parser = argparse.ArgumentParser(description="for testing purposes") + commands = parser.add_subparsers() + cli(commands) + + return parser + + +@pytest.fixture() +def logger(): + """Return the logger instance to use.""" + + log = logging.getLogger(__name__) + log.setLevel(logging.INFO) + + for handler in log.handlers: + log.removeHandler(handler) + + return log + + +@pytest.mark.parametrize( + "args, messages, expected", + [ + ( + [], + [dict(jsonrpc="2.0", id=1, method="initialize", params=dict())], + '{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}}\n', + ), + ( + ["-f", "{.|json-compact}"], + [dict(jsonrpc="2.0", id=1, method="initialize", params=dict())], + '{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}}\n', + ), + ( + ["-f", "{.|json}"], + [dict(jsonrpc="2.0", id=1, method="initialize", params=dict())], + "\n".join( + [ + "{", + ' "jsonrpc": "2.0",', + ' "id": 1,', + ' "method": "initialize",', + ' "params": {}', + "}", + "", + ] + ), + ), + ( + ["-f", "{.method|json}"], + [ + dict(jsonrpc="2.0", id=1, method="initialize", params=dict()), + dict(jsonrpc="2.0", id=1, result=dict()), + ], + "initialize\n", + ), + ( + ["-f", "{.id}"], + [ + dict(jsonrpc="2.0", id=1, method="initialize", params=dict()), + dict(jsonrpc="2.0", id=1, result=dict()), + ], + "1\n1\n", + ), + ], +) +def test_file_output( + tmp_path: pathlib.Path, + record: argparse.ArgumentParser, + logger: logging.Logger, + args: List[str], + messages: List[Dict[str, Any]], + expected: str, +): + """Ensure that we can log to files correctly. + + Parameters + ---------- + tmp_path + pytest's ``tmp_path`` fixture + + record + The record command's cli parser + + logger + The logging instance to use + + messages + The messages to record + + expected + The expected file output. + """ + log = tmp_path / "log.json" + parsed_args = record.parse_args(["record", "--to-file", str(log), *args]) + + setup_file_output(parsed_args, logger) + + for message in messages: + logger.info("%s", message, extra={"source": "client"}) + + assert log.read_text() == expected