Skip to content

Commit 03ee3a1

Browse files
authored
feat: incremental build (#139)
In order to avoid compiling all the classes every test execution, neotest-java will: * read all the sources. * keep a file with all cached sources. * filter sources that have not changed since the last compilation. This feature is configurable through the option `incremental_build`, whose default is `true`. Also added a test timer indicating how much did the tests last.
1 parent d624c32 commit 03ee3a1

File tree

10 files changed

+248
-173
lines changed

10 files changed

+248
-173
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ require("neotest").setup({
6868
ignore_wrapper = false, -- whether to ignore maven/gradle wrapper
6969
junit_jar = nil,
7070
-- default: .local/share/nvim/neotest-java/junit-platform-console-standalone-[version].jar
71+
incremental_build = true
7172
})
7273
}
7374
})
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
local log = require("neotest-java.logger")
2+
local nio = require("nio")
3+
local Job = require("plenary.job")
4+
local lib = require("neotest.lib")
5+
local binaries = require("neotest-java.command.binaries")
6+
local build_tools = require("neotest-java.build_tool")
7+
local read_file = require("neotest-java.util.read_file")
8+
local write_file = require("neotest-java.util.write_file")
9+
local config = require("neotest-java.context_holder").get_context().config
10+
11+
local Compiler = {}
12+
13+
---@param sources table<string>
14+
---@param cache_dir string
15+
---@return table<string> changed_sources
16+
local filter_unchanged_sources = function(sources, cache_dir)
17+
---@type { hash: string, source: string }
18+
local source_hashmap
19+
local success
20+
local cache_filepath = string.format("%s/cached_classes.json", cache_dir)
21+
22+
success, source_hashmap = pcall(function()
23+
return nio.fn.json_decode(read_file(cache_filepath))
24+
end)
25+
26+
if not success then
27+
source_hashmap = {}
28+
end
29+
30+
local changed_sources = {}
31+
for _, source in ipairs(sources) do
32+
local hash = nio.fn.sha256(read_file(source))
33+
34+
-- if file not seen yet
35+
-- or file content has changed
36+
if not source_hashmap[source] or source_hashmap[source] ~= hash then
37+
-- add
38+
source_hashmap[source] = hash
39+
40+
changed_sources[#changed_sources + 1] = source
41+
end
42+
end
43+
44+
log.debug("changed_sources: " .. vim.inspect(changed_sources))
45+
46+
write_file(cache_filepath, nio.fn.json_encode(source_hashmap))
47+
48+
return changed_sources
49+
end
50+
51+
Compiler.compile_sources = function(project_type)
52+
local build_tool = build_tools.get(project_type)
53+
54+
local sources = config.incremental_build
55+
and filter_unchanged_sources(build_tool.get_sources(), build_tool.get_output_dir())
56+
or build_tool.get_sources()
57+
58+
if #sources == 0 then
59+
return -- skipping as there are no sources to compile
60+
end
61+
62+
lib.notify("Compiling main sources")
63+
64+
build_tool.prepare_classpath()
65+
66+
local compilation_errors = {}
67+
local status_code = 0
68+
local output_dir = build_tool.get_output_dir()
69+
local source_compilation_command_exited = nio.control.event()
70+
local source_compilation_args = {
71+
"-g",
72+
"-Xlint:none",
73+
"-parameters",
74+
"-d",
75+
output_dir .. "/classes",
76+
"@" .. output_dir .. "/cp_arguments.txt",
77+
}
78+
for _, source in ipairs(sources) do
79+
table.insert(source_compilation_args, source)
80+
end
81+
Job:new({
82+
command = binaries.javac(),
83+
args = source_compilation_args,
84+
on_stderr = function(_, data)
85+
table.insert(compilation_errors, data)
86+
end,
87+
on_exit = function(_, code)
88+
status_code = code
89+
if code == 0 then
90+
source_compilation_command_exited.set()
91+
else
92+
source_compilation_command_exited.set()
93+
lib.notify("Error compiling sources", vim.log.levels.ERROR)
94+
log.error("test compilation error args: ", vim.inspect(source_compilation_args))
95+
error("Error compiling sources: " .. table.concat(compilation_errors, "\n"))
96+
end
97+
end,
98+
}):start()
99+
source_compilation_command_exited.wait()
100+
assert(status_code == 0, "Error compiling sources")
101+
end
102+
103+
Compiler.compile_test_sources = function(project_type)
104+
local build_tool = build_tools.get(project_type)
105+
106+
local sources = config.incremental_build
107+
and filter_unchanged_sources(build_tool.get_test_sources(), build_tool.get_output_dir())
108+
or build_tool.get_test_sources()
109+
110+
if #sources == 0 then
111+
return -- skipping as there are no sources to compile
112+
end
113+
114+
lib.notify("Compiling test sources")
115+
116+
local compilation_errors = {}
117+
local status_code = 0
118+
local output_dir = build_tool.get_output_dir()
119+
120+
local test_compilation_command_exited = nio.control.event()
121+
local test_sources_compilation_args = {
122+
"-g",
123+
"-Xlint:none",
124+
"-parameters",
125+
"-d",
126+
output_dir .. "/classes",
127+
("@%s/cp_arguments.txt"):format(output_dir),
128+
}
129+
for _, source in ipairs(sources) do
130+
table.insert(test_sources_compilation_args, source)
131+
end
132+
133+
Job:new({
134+
command = binaries.javac(),
135+
args = test_sources_compilation_args,
136+
on_stderr = function(_, data)
137+
table.insert(compilation_errors, data)
138+
end,
139+
on_exit = function(_, code)
140+
status_code = code
141+
test_compilation_command_exited.set()
142+
if code == 0 then
143+
-- do nothing
144+
else
145+
lib.notify("Error compiling test sources", vim.log.levels.ERROR)
146+
log.error("test compilation error args: ", vim.inspect(test_sources_compilation_args))
147+
error("Error compiling test sources: " .. table.concat(compilation_errors, "\n"))
148+
end
149+
end,
150+
}):start()
151+
test_compilation_command_exited.wait()
152+
assert(status_code == 0, "Error compiling test sources")
153+
end
154+
155+
return Compiler

lua/neotest-java/build_tool/init.lua

Lines changed: 0 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -26,93 +26,6 @@ build_tools.get = function(project_type)
2626
return build_tools[project_type]
2727
end
2828

29-
build_tools.compile_sources = function(project_type)
30-
lib.notify("Compiling sources", vim.log.levels.INFO)
31-
32-
local build_tool = build_tools.get(project_type)
33-
build_tool.prepare_classpath()
34-
35-
local compilation_errors = {}
36-
local status_code = 0
37-
local sources = build_tool.get_sources()
38-
local output_dir = build_tool.get_output_dir()
39-
local source_compilation_command_exited = nio.control.event()
40-
local source_compilation_args = {
41-
"-g",
42-
"-Xlint:none",
43-
"-parameters",
44-
"-d",
45-
output_dir .. "/classes",
46-
"@" .. output_dir .. "/cp_arguments.txt",
47-
}
48-
for _, source in ipairs(sources) do
49-
table.insert(source_compilation_args, source)
50-
end
51-
Job:new({
52-
command = binaries.javac(),
53-
args = source_compilation_args,
54-
on_stderr = function(_, data)
55-
table.insert(compilation_errors, data)
56-
end,
57-
on_exit = function(_, code)
58-
status_code = code
59-
if code == 0 then
60-
source_compilation_command_exited.set()
61-
else
62-
source_compilation_command_exited.set()
63-
lib.notify("Error compiling sources", vim.log.levels.ERROR)
64-
log.error("test compilation error args: ", vim.inspect(source_compilation_args))
65-
error("Error compiling sources: " .. table.concat(compilation_errors, "\n"))
66-
end
67-
end,
68-
}):start()
69-
source_compilation_command_exited.wait()
70-
assert(status_code == 0, "Error compiling sources")
71-
end
72-
73-
build_tools.compile_test_sources = function(project_type)
74-
lib.notify("Compiling test sources", vim.log.levels.INFO)
75-
local build_tool = build_tools.get(project_type)
76-
77-
local compilation_errors = {}
78-
local status_code = 0
79-
local output_dir = build_tool.get_output_dir()
80-
81-
local test_compilation_command_exited = nio.control.event()
82-
local test_sources_compilation_args = {
83-
"-g",
84-
"-Xlint:none",
85-
"-parameters",
86-
"-d",
87-
output_dir .. "/classes",
88-
("@%s/cp_arguments.txt"):format(output_dir),
89-
}
90-
for _, source in ipairs(build_tool.get_test_sources()) do
91-
table.insert(test_sources_compilation_args, source)
92-
end
93-
94-
Job:new({
95-
command = binaries.javac(),
96-
args = test_sources_compilation_args,
97-
on_stderr = function(_, data)
98-
table.insert(compilation_errors, data)
99-
end,
100-
on_exit = function(_, code)
101-
status_code = code
102-
test_compilation_command_exited.set()
103-
if code == 0 then
104-
-- do nothing
105-
else
106-
lib.notify("Error compiling test sources", vim.log.levels.ERROR)
107-
log.error("test compilation error args: ", vim.inspect(test_sources_compilation_args))
108-
error("Error compiling test sources: " .. table.concat(compilation_errors, "\n"))
109-
end
110-
end,
111-
}):start()
112-
test_compilation_command_exited.wait()
113-
assert(status_code == 0, "Error compiling test sources")
114-
end
115-
11629
---@param command string
11730
---@param args string[]
11831
---@return nio.control.Event

lua/neotest-java/command/junit_command_builder.lua

Lines changed: 18 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -103,83 +103,10 @@ local CommandBuilder = {
103103
self._reports_dir = reports_dir
104104
end,
105105

106-
--- @return string @command to run
107-
--- @deprecated
108-
build = function(self)
109-
local build_tool = build_tools.get(self._project_type)
110-
local build_dir = build_tool.get_output_dir()
111-
local output_dir = build_dir .. "/classes"
112-
local resources = table.concat(build_tool.get_resources(), ":")
113-
local source_classes = build_tool.get_sources()
114-
local test_classes = build_tool.get_test_sources()
115-
116-
local selectors = {}
117-
for _, v in ipairs(self._test_references) do
118-
if v.type == "test" then
119-
table.insert(selectors, "-m=" .. v.qualified_name .. "#" .. v.method_name)
120-
elseif v.type == "file" then
121-
table.insert(selectors, "-c=" .. v.qualified_name)
122-
elseif v.type == "dir" then
123-
selectors = "-p=" .. v.qualified_name
124-
end
125-
end
126-
assert(#selectors ~= 0, "junit command has to have a selector")
127-
128-
build_tool.prepare_classpath()
129-
130-
local source_compilation_command = [[
131-
{{javac}} -Xlint:none -parameters -d {{output_dir}} {{classpath_arg}} {{source_classes}}
132-
]]
133-
local test_compilation_command = [[
134-
{{javac}} -Xlint:none -parameters -d {{output_dir}} {{classpath_arg}} {{test_classes}}
135-
]]
136-
137-
local test_execution_command = [[
138-
{{java}} -jar {{junit_jar}} execute {{classpath_arg}} {{selectors}}
139-
--fail-if-no-tests --reports-dir={{reports_dir}} --disable-banner --details=testfeed --config=junit.platform.output.capture.stdout=true
140-
]]
141-
142-
-- combine commands sequentially
143-
local command = table.concat({
144-
source_compilation_command,
145-
test_compilation_command,
146-
test_execution_command,
147-
}, " && ")
148-
149-
-- replace placeholders
150-
local placeholders = {
151-
["{{javac}}"] = javac(),
152-
["{{java}}"] = java(),
153-
["{{junit_jar}}"] = self._junit_jar,
154-
["{{resources}}"] = resources,
155-
["{{output_dir}}"] = output_dir,
156-
["{{classpath_arg}}"] = ("@%s/cp_arguments.txt"):format(build_dir),
157-
["{{reports_dir}}"] = self._reports_dir,
158-
["{{selectors}}"] = table.concat(selectors, " "),
159-
["{{source_classes}}"] = table.concat(source_classes, " "),
160-
["{{test_classes}}"] = table.concat(test_classes, " "),
161-
}
162-
iter(placeholders):each(function(k, v)
163-
command = command:gsub(k, v)
164-
end)
165-
166-
-- remove extra spaces
167-
command = command:gsub("%s+", " ")
168-
169-
log.info("Command: " .. command)
170-
171-
command = stop_command_when_line_containing(command, "Test run finished")
172-
173-
command = wrap_command_as_bash(command)
174-
175-
return command
176-
end,
177-
178106
--- @param port? number
179107
--- @return { command: string, args: string[] }
180108
build_junit = function(self, port)
181109
assert(self._test_references, "test_references cannot be nil")
182-
assert(port, "port cannot be nil")
183110

184111
local build_tool = build_tools.get(self._project_type)
185112

@@ -198,8 +125,6 @@ local CommandBuilder = {
198125
local junit_command = {
199126
command = java(),
200127
args = {
201-
"-Xdebug",
202-
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=0.0.0.0:" .. port,
203128
"-jar",
204129
self._junit_jar,
205130
"execute",
@@ -215,8 +140,26 @@ local CommandBuilder = {
215140
for _, v in ipairs(selectors) do
216141
table.insert(junit_command.args, v)
217142
end
143+
144+
-- add debug arguments if debug port is specified
145+
if port then
146+
table.insert(junit_command.args, 1, "-Xdebug")
147+
table.insert(
148+
junit_command.args,
149+
1,
150+
"-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=0.0.0.0:" .. port
151+
)
152+
end
153+
218154
return junit_command
219155
end,
156+
157+
--- @param port? number
158+
--- @return { command: string, args: string[] }
159+
build_to_string = function(self, port)
160+
local c = self:build_junit(port)
161+
return c.command .. " " .. table.concat(c.args, " ")
162+
end,
220163
}
221164

222165
return CommandBuilder

lua/neotest-java/context_holder.lua

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
local log = require("neotest-java.logger")
2-
local nio = require("nio")
32

43
---@type neotest-java.ConfigOpts
54
local default_config = {
65
ignore_wrapper = false,
76
junit_jar = vim.fn.stdpath("data") .. "/neotest-java/junit-platform-console-standalone-1.10.1.jar",
7+
incremental_build = true,
88
}
99

1010
---@type neotest-java.Context
@@ -28,6 +28,7 @@ return {
2828
---@class neotest-java.ConfigOpts
2929
---@field ignore_wrapper boolean
3030
---@field junit_jar string
31+
---@field incremental_build boolean
3132

3233
---@class neotest-java.Context
3334
---@field config neotest-java.ConfigOpts

0 commit comments

Comments
 (0)