From aaf83100b632d7142534ea2576eeada7e7d1e29f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B3n=C3=A1n=20Carrigan?= Date: Sun, 17 Jul 2022 22:22:29 +0100 Subject: [PATCH] feat: streamed results --- lua/neotest-python/init.lua | 22 ++++++++++++++++++++++ neotest_python/__init__.py | 16 +++++++++++++++- neotest_python/pytest.py | 17 ++++++++++++----- neotest_python/unittest.py | 5 +++-- 4 files changed, 52 insertions(+), 8 deletions(-) diff --git a/lua/neotest-python/init.lua b/lua/neotest-python/init.lua index 944b745..918a54d 100644 --- a/lua/neotest-python/init.lua +++ b/lua/neotest-python/init.lua @@ -92,12 +92,20 @@ end function PythonNeotestAdapter.build_spec(args) local position = args.tree:data() local results_path = async.fn.tempname() + local stream_path = async.fn.tempname() + local x = io.open(stream_path, "w") + x:write("") + x:close() + local root = PythonNeotestAdapter.root(position.path) local python = base.get_python_command(root) local runner = get_runner(python) + local stream_data, stop_stream = lib.files.stream_lines(stream_path) local script_args = vim.tbl_flatten({ "--results-file", results_path, + "--stream-file", + stream_path, "--runner", runner, "--", @@ -112,11 +120,24 @@ function PythonNeotestAdapter.build_spec(args) script_args, }) local strategy_config = get_strategy_config(args.strategy, python, python_script, script_args) + ---@type neotest.RunSpec return { command = command, context = { results_path = results_path, + stop_stream = stop_stream, }, + stream = function() + return function() + local lines = stream_data() + local results = {} + for _, line in ipairs(lines) do + local result = vim.json.decode(line, { luanil = { object = true } }) + results[result.id] = result.result + end + return results + end + end, strategy = strategy_config, } end @@ -126,6 +147,7 @@ end ---@param result neotest.StrategyResult ---@return neotest.Result[] function PythonNeotestAdapter.results(spec, result) + spec.context.stop_stream() local success, data = pcall(lib.files.read, spec.context.results_path) if not success then data = "{}" diff --git a/neotest_python/__init__.py b/neotest_python/__init__.py index 1e5e999..36d97d0 100644 --- a/neotest_python/__init__.py +++ b/neotest_python/__init__.py @@ -3,6 +3,8 @@ import json from enum import Enum from typing import List +from neotest_python.base import NeotestResult + class TestRunner(str, Enum): PYTEST = "pytest" @@ -29,12 +31,24 @@ parser.add_argument( required=True, help="File to store result JSON in", ) +parser.add_argument( + "--stream-file", + dest="stream_file", + required=True, + help="File to stream result JSON to", +) parser.add_argument("args", nargs="*") def main(argv: List[str]): args = parser.parse_args(argv) adapter = get_adapter(TestRunner(args.runner)) - results = adapter.run(args.args) + with open(args.stream_file, "w") as stream_file: + + def stream(pos_id: str, result: NeotestResult): + stream_file.write(json.dumps({"id": pos_id, "result": result}) + "\n") + stream_file.flush() + + results = adapter.run(args.args, stream) with open(args.results_file, "w") as results_file: json.dump(results, results_file) diff --git a/neotest_python/pytest.py b/neotest_python/pytest.py index 46e04b9..39e4784 100644 --- a/neotest_python/pytest.py +++ b/neotest_python/pytest.py @@ -1,6 +1,6 @@ from io import StringIO from pathlib import Path -from typing import TYPE_CHECKING, Dict, List, Optional, cast +from typing import TYPE_CHECKING, Callable, Dict, List, Optional from .base import NeotestAdapter, NeotestError, NeotestResult, NeotestResultStatus @@ -32,7 +32,9 @@ class PytestNeotestAdapter(NeotestAdapter): buffer.seek(0) return buffer.read() - def run(self, args: List[str]) -> Dict[str, NeotestResult]: + def run( + self, args: List[str], stream: Callable[[str, NeotestResult], None] + ) -> Dict[str, NeotestResult]: results: Dict[str, NeotestResult] = {} pytest_config: "Config" from _pytest._code.code import ExceptionChainRepr @@ -52,7 +54,7 @@ class PytestNeotestAdapter(NeotestAdapter): file_path, *name_path = report.nodeid.split("::") abs_path = str(Path(pytest_config.rootpath, file_path)) test_name, *namespaces = reversed(name_path) - valid_test_name, *_ = test_name.split("[") # ] + valid_test_name, *params = test_name.split("[") # ] errors: List[NeotestError] = [] short = self.get_short_output(pytest_config, report) @@ -75,9 +77,11 @@ class PytestNeotestAdapter(NeotestAdapter): errors.append({"message": error_message, "line": error_line}) else: # TODO: Figure out how these are returned and how to represent - raise Exception("Unhandled error type, please report to neotest-python repo") + raise Exception( + "Unhandled error type, please report to neotest-python repo" + ) pos_id = "::".join([abs_path, *namespaces, valid_test_name]) - results[pos_id] = self.update_result( + result = self.update_result( results.get(pos_id), { "short": short, @@ -85,6 +89,9 @@ class PytestNeotestAdapter(NeotestAdapter): "errors": errors, }, ) + if not params: + stream(pos_id, result) + results[pos_id] = result import pytest diff --git a/neotest_python/unittest.py b/neotest_python/unittest.py index ee0035f..a275218 100644 --- a/neotest_python/unittest.py +++ b/neotest_python/unittest.py @@ -5,7 +5,7 @@ import traceback import unittest from pathlib import Path from types import TracebackType -from typing import Any, Dict, Iterator, List, Tuple +from typing import Any, Dict, List, Tuple from unittest import TestCase, TestResult, TestSuite from unittest.runner import TextTestResult, TextTestRunner @@ -42,7 +42,8 @@ class UnittestNeotestAdapter(NeotestAdapter): relative_dotted = relative_stem.replace(os.sep, ".") return [".".join([relative_dotted, *child_ids])] - def run(self, args: List[str]) -> Dict: + # TODO: Stream results + def run(self, args: List[str], _) -> Dict: results = {} errs: Dict[str, Tuple[Exception, Any, TracebackType]] = {}