Skip to content
This repository was archived by the owner on Nov 30, 2025. It is now read-only.

Commit aeec296

Browse files
authored
feat: add test report (#3)
* feat: add test result parser * feat: handle report parsing when the test is failed * feat: add report viewer * feat: store the last test report * fix(test): test failures * fix(test): tests are hanging due to command loading error * refactor: remove test_plugins dir
1 parent 7672c05 commit aeec296

22 files changed

Lines changed: 529 additions & 78 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
vendor/plenary.nvim
2+
.test_plugins

Makefile

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
PREPARE_CONFIG=tests/prepare-config.lua
2-
TEST_CONFIG=tests/test-config.lua
1+
SETUP_TEST=tests/setup-test.lua
2+
BEFORE_TEST=tests/before-test.lua
33
TESTS_DIR=tests/
44

55
.PHONY: test
66

77
test:
88
@nvim \
99
--headless \
10-
-u ${PREPARE_CONFIG} \
11-
"+PlenaryBustedDirectory ${TESTS_DIR} { minimal_init = '${TEST_CONFIG}' }"
10+
-u ${SETUP_TEST} \
11+
"+PlenaryBustedDirectory ${TESTS_DIR} { minimal_init = '${BEFORE_TEST}' }"

lua/java-test.lua

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
local M = {}
2+
3+
return M

lua/java-test/reports/junit.lua

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
local class = require('java-core.utils.class')
2+
local log = require('java-core.utils.log')
3+
4+
---@class java_test.JUnitTestReport
5+
---@field private conn uv_tcp_t
6+
---@field private result_parser java_test.TestParser
7+
---@field private result_parser_fac java_test.TestParserFactory
8+
---@field private report_viewer java_test.ReportViewer
9+
---@overload fun(result_parser_factory: java_test.TestParserFactory, test_viewer: java_test.ReportViewer)
10+
local JUnitReport = class()
11+
12+
---Init
13+
---@param result_parser_factory java_test.TestParserFactory
14+
function JUnitReport:_init(result_parser_factory, report_viewer)
15+
self.conn = nil
16+
self.result_parser_fac = result_parser_factory
17+
self.report_viewer = report_viewer
18+
end
19+
20+
---Returns the test results
21+
---@return java_test.TestResults[]
22+
function JUnitReport:get_results()
23+
return self.result_parser:get_test_details()
24+
end
25+
26+
---Shows the test report
27+
function JUnitReport:show_report()
28+
self.report_viewer:show(self:get_results())
29+
end
30+
31+
---Returns a stream reader function
32+
---@param conn uv_tcp_t
33+
---@return fun(err: string, buffer: string) # callback function
34+
function JUnitReport:get_stream_reader(conn)
35+
self.conn = conn
36+
self.result_parser = self.result_parser_fac:get_parser()
37+
38+
return vim.schedule_wrap(function(err, buffer)
39+
if err then
40+
self:on_error(err)
41+
self:on_close()
42+
self.conn:close()
43+
return
44+
end
45+
46+
if buffer then
47+
self:on_update(buffer)
48+
else
49+
self:on_close()
50+
self.conn:close()
51+
end
52+
end)
53+
end
54+
55+
---Runs on connection update
56+
---@private
57+
---@param text string
58+
function JUnitReport:on_update(text)
59+
self.result_parser:parse(text)
60+
end
61+
62+
---Runs on connection close
63+
---@private
64+
function JUnitReport:on_close() end
65+
66+
---Runs on connection error
67+
---@private
68+
---@param err string error
69+
function JUnitReport:on_error(err)
70+
log.error('Error while running test', err)
71+
end
72+
73+
return JUnitReport
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---@enum java_test.TestExecutionStatus
2+
local TestStatus = {
3+
Started = 'started',
4+
Ended = 'ended',
5+
}
6+
7+
return TestStatus
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---@enum MessageId
2+
local MessageId = {
3+
-- Notification about a test inside the test suite.
4+
-- TEST_TREE + testId + "," + testName + "," + isSuite + "," + testCount + "," + isDynamicTest +
5+
-- "," + parentId + "," + displayName + "," + parameterTypes + "," + uniqueId
6+
7+
-- isSuite = "true" or "false"
8+
-- isDynamicTest = "true" or "false"
9+
-- parentId = the unique id of its parent if it is a dynamic test, otherwise can be "-1"
10+
-- displayName = the display name of the test
11+
-- parameterTypes = comma-separated list of method parameter types if applicable, otherwise an
12+
-- empty string
13+
-- uniqueId = the unique ID of the test provided by JUnit launcher, otherwise an empty string
14+
15+
TestTree = '%TSTTREE',
16+
TestStart = '%TESTS',
17+
TestEnd = '%TESTE',
18+
TestFailed = '%FAILED',
19+
TestError = '%ERROR',
20+
ExpectStart = '%EXPECTS',
21+
ExpectEnd = '%EXPECTE',
22+
ActualStart = '%ACTUALS',
23+
ActualEnd = '%ACTUALE',
24+
TraceStart = '%TRACES',
25+
TraceEnd = '%TRACEE',
26+
IGNORE_TEST_PREFIX = '@Ignore: ',
27+
ASSUMPTION_FAILED_TEST_PREFIX = '@AssumptionFailure: ',
28+
}
29+
30+
--[[
31+
*************
32+
%TESTC 2 v2
33+
%TSTTREE2,com.example.demo.DemoApplicationTests,true,2,false,1,DemoApplicationTests,,[engine:junit-jupiter]/[class:com.example.demo.DemoApplicationTests]
34+
%TSTTREE3,anotherTest(com.example.demo.DemoApplicationTests),false,1,false,2,anotherTest(),,[engine:junit-jupiter]/[class:com.example.demo.DemoApplicationTests]/[method:anotherTest()]
35+
%TSTTREE4,contextLoads(com.example.demo.DemoApplicationTests),false,1,false,2,contextLoads(),,[engine:junit-jupiter]/[class:com.example.demo.DemoApplicationTests]/[method:contextLoads()]
36+
%TESTS 3,anotherTest(com.example.demo.DemoApplicationTests)
37+
*************
38+
%TESTE 3,anotherTest(com.example.demo.DemoApplicationTests)
39+
*************
40+
%TESTS 4,contextLoads(com.example.demo.DemoApplicationTests)
41+
*************
42+
%TESTE 4,contextLoads(com.example.demo.DemoApplicationTests)
43+
%RUNTIME2281
44+
--]]
45+
46+
return MessageId
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
local class = require('java-core.utils.class')
2+
local TestParser = require('java-test.results.result-parser')
3+
4+
---@class java_test.TestParserFactory
5+
local TestParserFactory = class()
6+
7+
---Returns a test parser of given type
8+
---@param args any
9+
---@return java_test.TestParser
10+
function TestParserFactory.get_parser(args)
11+
return TestParser()
12+
end
13+
14+
return TestParserFactory
Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
local class = require('java-core.utils.class')
2+
3+
local MessageId = require('java-test.results.message-id')
4+
local TestStatus = require('java-test.results.result-status')
5+
local TestExecStatus = require('java-test.results.execution-status')
6+
7+
---@class java_test.TestParser
8+
---@field private test_details java_test.TestResults[]
9+
local TestParser = class()
10+
11+
---Init
12+
---@private
13+
function TestParser:_init()
14+
self.test_details = {}
15+
end
16+
17+
---@private
18+
TestParser.node_parsers = {
19+
[MessageId.TestTree] = 'parse_test_tree',
20+
[MessageId.TestStart] = 'parse_test_start',
21+
[MessageId.TestEnd] = 'parse_test_end',
22+
[MessageId.TestFailed] = 'parse_test_failed',
23+
}
24+
25+
---@private
26+
TestParser.strtobool = {
27+
['true'] = true,
28+
['false'] = false,
29+
}
30+
31+
---Parse a given text into test details
32+
---@param text string test result buffer
33+
function TestParser:parse(text)
34+
if text:sub(-1) ~= '\n' then
35+
text = text .. '\n'
36+
end
37+
38+
local line_iter = text:gmatch('(.-)\n')
39+
40+
local line = line_iter()
41+
42+
while line ~= nil do
43+
local message_id = line:sub(1, 8):gsub('%s+', '')
44+
local content = line:sub(9)
45+
46+
local node_parser = TestParser.node_parsers[message_id]
47+
48+
if node_parser then
49+
local data = vim.split(content, ',', { plain = true, trimempty = true })
50+
51+
if self[TestParser.node_parsers[message_id]] then
52+
self[TestParser.node_parsers[message_id]](self, data, line_iter)
53+
end
54+
end
55+
56+
line = line_iter()
57+
end
58+
end
59+
60+
---Returns the parsed test details
61+
---@return java_test.TestResults # parsed test details
62+
function TestParser:get_test_details()
63+
return self.test_details
64+
end
65+
66+
---@private
67+
function TestParser:parse_test_tree(data)
68+
local node = {
69+
test_id = tonumber(data[1]),
70+
test_name = data[2],
71+
is_suite = TestParser.strtobool[data[3]],
72+
test_count = tonumber(data[4]),
73+
is_dynamic_test = TestParser.strtobool[data[5]],
74+
parent_id = tonumber(data[6]),
75+
display_name = data[7],
76+
parameter_types = data[8],
77+
unique_id = data[9],
78+
}
79+
80+
local parent = self:find_result_node(node.parent_id)
81+
82+
if not parent then
83+
table.insert(self.test_details, node)
84+
else
85+
parent.children = parent.children or {}
86+
table.insert(parent.children, node)
87+
end
88+
end
89+
90+
---@private
91+
function TestParser:parse_test_start(data)
92+
local test_id = tonumber(data[1])
93+
local node = self:find_result_node(test_id)
94+
assert(node)
95+
node.result = {}
96+
node.result.execution = TestExecStatus.Started
97+
end
98+
99+
---@private
100+
function TestParser:parse_test_end(data)
101+
local test_id = tonumber(data[1])
102+
local node = self:find_result_node(test_id)
103+
assert(node)
104+
node.result.execution = TestExecStatus.Ended
105+
end
106+
107+
---@private
108+
function TestParser:parse_test_failed(data, line_iter)
109+
local test_id = tonumber(data[1])
110+
local node = self:find_result_node(test_id)
111+
assert(node)
112+
113+
node.result.status = TestStatus.Failed
114+
115+
while true do
116+
local line = line_iter()
117+
118+
if line == nil then
119+
break
120+
end
121+
122+
-- EXPECTED
123+
if vim.startswith(line, MessageId.ExpectStart) then
124+
node.result.expected =
125+
self:get_content_until_end_tag(MessageId.ExpectEnd, line_iter)
126+
127+
-- ACTUAL
128+
elseif vim.startswith(line, MessageId.ActualStart) then
129+
node.result.actual =
130+
self:get_content_until_end_tag(MessageId.ActualEnd, line_iter)
131+
132+
-- TRACE
133+
elseif vim.startswith(line, MessageId.TraceStart) then
134+
node.result.trace =
135+
self:get_content_until_end_tag(MessageId.TraceEnd, line_iter)
136+
end
137+
end
138+
end
139+
140+
---@private
141+
function TestParser:get_content_until_end_tag(end_tag, line_iter)
142+
local content = {}
143+
144+
while true do
145+
local line = line_iter()
146+
147+
if line == nil or vim.startswith(line, end_tag) then
148+
break
149+
end
150+
151+
table.insert(content, line)
152+
end
153+
154+
return content
155+
end
156+
157+
---@private
158+
function TestParser:find_result_node(id)
159+
local function find_node(nodes)
160+
if not nodes or #nodes == 0 then
161+
return
162+
end
163+
164+
for _, node in ipairs(nodes) do
165+
if node.test_id == id then
166+
return node
167+
end
168+
169+
local _node = find_node(node.children)
170+
171+
if _node then
172+
return _node
173+
end
174+
end
175+
end
176+
177+
return find_node(self.test_details)
178+
end
179+
180+
return TestParser
181+
182+
---@class java_test.TestResultExecutionDetails
183+
---@field actual string[] lines
184+
---@field expected string[] lines
185+
---@field status java_test.TestStatus
186+
---@field execution java_test.TestExecutionStatus
187+
---@field trace string[] lines
188+
189+
---@class java_test.TestResults
190+
---@field display_name string
191+
---@field is_dynamic_test boolean
192+
---@field is_suite boolean
193+
---@field parameter_types string
194+
---@field parent_id integer
195+
---@field test_count integer
196+
---@field test_id integer
197+
---@field test_name string
198+
---@field unique_id string
199+
---@field result java_test.TestResultExecutionDetails
200+
---@field children java_test.TestResults[]
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---@enum java_test.TestStatus
2+
local TestStatus = {
3+
Failed = 'failed',
4+
Skipped = 'skipped',
5+
}
6+
7+
return TestStatus

0 commit comments

Comments
 (0)