From 5ab6e7e4b5303409b18e86b403d1393d9aed3bf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Cas=C3=ADa?= <31012661+rcasia@users.noreply.github.com> Date: Mon, 5 Jan 2026 02:12:08 +0100 Subject: [PATCH 1/6] feat: make 6.0.1 be the latest pinned version --- lua/neotest-java/default_config.lua | 30 ++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/lua/neotest-java/default_config.lua b/lua/neotest-java/default_config.lua index 34b36d2f..855effd9 100644 --- a/lua/neotest-java/default_config.lua +++ b/lua/neotest-java/default_config.lua @@ -1,8 +1,23 @@ local Path = require("neotest-java.model.path") -local DEFAULT_VERSION = "1.10.1" -local JUNIT_JAR_FILE_NAME = "junit-platform-console-standalone-" .. DEFAULT_VERSION .. ".jar" -local DEFAULT_JUNIT_JAR_PATH = Path(vim.fn.stdpath("data")):append("neotest-java"):append(JUNIT_JAR_FILE_NAME) +local JUNIT_JAR_FILE_NAME = function(version) + return "junit-platform-console-standalone-" .. version .. ".jar" +end +local DEFAULT_JUNIT_JAR_PATH = function(version) + return Path(vim.fn.stdpath("data")):append("neotest-java"):append(JUNIT_JAR_FILE_NAME(version)) +end + +local SUPPORTED_VERSIONS = { + { + version = "6.0.1", + sha256 = "3009120b7953bfe63add272e65b2bbeca0d41d0dfd8dea605201db15b640e0ff", + }, + { + version = "1.10.1", + sha256 = "b42eaa53d13576d17db5fb8b280722a6ae9e36daf95f4262bc6e96d4cb20725f", + }, +} +local LATEST_PINNED_VERSION = SUPPORTED_VERSIONS[1] --- @class neotest-java.JunitVersion --- @field version string @@ -18,14 +33,11 @@ local DEFAULT_JUNIT_JAR_PATH = Path(vim.fn.stdpath("data")):append("neotest-java ---@type neotest-java.ConfigOpts local default_config = { - default_junit_jar_filepath = DEFAULT_JUNIT_JAR_PATH, - junit_jar = DEFAULT_JUNIT_JAR_PATH, + default_junit_jar_filepath = DEFAULT_JUNIT_JAR_PATH(LATEST_PINNED_VERSION.version), + junit_jar = DEFAULT_JUNIT_JAR_PATH(LATEST_PINNED_VERSION.version), + default_junit_jar_version = LATEST_PINNED_VERSION, jvm_args = {}, incremental_build = true, - default_junit_jar_version = { - version = DEFAULT_VERSION, - sha256 = "b42eaa53d13576d17db5fb8b280722a6ae9e36daf95f4262bc6e96d4cb20725f", - }, test_classname_patterns = { "^.*Tests?$", "^.*IT$", From 8de91a3ca0d1274b67bf74522ed519ca0705b5e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Cas=C3=ADa?= <31012661+rcasia@users.noreply.github.com> Date: Tue, 20 Jan 2026 22:56:26 +0100 Subject: [PATCH 2/6] feat: support junit jar updates --- README.md | 1 + lua/neotest-java/context_holder.lua | 2 + lua/neotest-java/default_config.lua | 11 + lua/neotest-java/init.lua | 61 +++- lua/neotest-java/install.lua | 184 +++++++--- .../util/junit_version_detector.lua | 106 ++++++ tests/unit/init_spec.lua | 27 ++ tests/unit/install_spec.lua | 323 ++++++++++++++++++ tests/unit/junit_version_detector_spec.lua | 149 ++++++++ 9 files changed, 809 insertions(+), 55 deletions(-) create mode 100644 lua/neotest-java/util/junit_version_detector.lua create mode 100644 tests/unit/install_spec.lua create mode 100644 tests/unit/junit_version_detector_spec.lua diff --git a/README.md b/README.md index 18dcdc4b..3fd56074 100644 --- a/README.md +++ b/README.md @@ -84,6 +84,7 @@ | `junit_jar` | `string?` | `stdpath("data") .. /nvim/neotest-java/junit-platform-console-standalone-[version].jar` | Path to the JUnit Platform Console standalone JAR. | | `jvm_args` | `string[]` | `{}` | Additional JVM arguments passed when running tests. | | `incremental_build` | `boolean` | `true` | Enable incremental compilation before running tests. | +| `disable_update_notifications` | `boolean` | `false` | Disable notifications about available JUnit jar updates. | | `test_classname_patterns` | `string[]` | `{"^.*Tests?$", "^.*IT$", "^.*Spec$"}` | Regular expressions used to include only classes whose names match these patterns. Classes not matching any pattern will be ignored. | ## :octocat: Contributing diff --git a/lua/neotest-java/context_holder.lua b/lua/neotest-java/context_holder.lua index c0986a22..cc95be01 100644 --- a/lua/neotest-java/context_holder.lua +++ b/lua/neotest-java/context_holder.lua @@ -2,4 +2,6 @@ return { --- @type neotest-java.Adapter | nil adapter = nil, + --- @type boolean + update_notification_shown = false, } diff --git a/lua/neotest-java/default_config.lua b/lua/neotest-java/default_config.lua index 855effd9..1b8eb3b5 100644 --- a/lua/neotest-java/default_config.lua +++ b/lua/neotest-java/default_config.lua @@ -19,6 +19,12 @@ local SUPPORTED_VERSIONS = { } local LATEST_PINNED_VERSION = SUPPORTED_VERSIONS[1] +--- Get supported JUnit versions +--- @return table[] +local function get_supported_versions() + return SUPPORTED_VERSIONS +end + --- @class neotest-java.JunitVersion --- @field version string --- @field sha256 string @@ -30,6 +36,7 @@ local LATEST_PINNED_VERSION = SUPPORTED_VERSIONS[1] ---@field incremental_build boolean ---@field default_junit_jar_version neotest-java.JunitVersion ---@field test_classname_patterns string[] | nil +---@field disable_update_notifications boolean | nil ---@type neotest-java.ConfigOpts local default_config = { @@ -38,6 +45,7 @@ local default_config = { default_junit_jar_version = LATEST_PINNED_VERSION, jvm_args = {}, incremental_build = true, + disable_update_notifications = false, test_classname_patterns = { "^.*Tests?$", "^.*IT$", @@ -45,4 +53,7 @@ local default_config = { }, } +-- Export getter function for supported versions +default_config.get_supported_versions = get_supported_versions + return default_config diff --git a/lua/neotest-java/init.lua b/lua/neotest-java/init.lua index 80c4e181..28d64553 100644 --- a/lua/neotest-java/init.lua +++ b/lua/neotest-java/init.lua @@ -11,8 +11,10 @@ local ch = require("neotest-java.context_holder") local Path = require("neotest-java.model.path") local nio = require("nio") local logger = require("neotest-java.logger") -local install = require("neotest-java.install") local Binaries = require("neotest-java.command.binaries") +local version_detector = require("neotest-java.util.junit_version_detector") +local lib = require("neotest.lib") +local exists = require("neotest.lib.file").exists local DEFAULT_CONFIG = require("neotest-java.default_config") @@ -58,7 +60,7 @@ end --- @param deps? neotest-java.Dependencies --- @return neotest-java.Adapter local function NeotestJavaAdapter(config, deps) - config = config or {} + config = vim.tbl_extend("force", DEFAULT_CONFIG, config or {}) deps = deps or {} local _root_finder = deps and deps.root_finder or root_finder @@ -69,6 +71,27 @@ local function NeotestJavaAdapter(config, deps) -- create data directory if it doesn't exist mkdir(Path(vim.fn.stdpath("data")):append("neotest-java")) + -- Check for JUnit jar updates (only if user hasn't disabled notifications and not already shown) + if not config.disable_update_notifications and not ch.update_notification_shown then + local existing_version, _ = version_detector.detect_existing_version() + if existing_version then + local has_update, latest_version = version_detector.check_for_update(existing_version) + if has_update and latest_version then + -- Mark notification as shown to avoid duplicates + ch.update_notification_shown = true + -- Show notification about available update + lib.notify( + string.format( + "JUnit jar update available: %s → %s. Run :NeotestJava setup to upgrade. (Disable: set disable_update_notifications = true in config)", + existing_version.version, + latest_version.version + ), + "info" + ) + end + end + end + local cwd = vim.loop.cwd() --- @type neotest-java.Path|nil @@ -118,7 +141,37 @@ local function NeotestJavaAdapter(config, deps) return setmetatable({ install = function() - install(config) + local Installer = require("neotest-java.install") + local installer = Installer({ + exists = exists, + checksum = function(path) + local f = assert(io.open(path:to_string(), "rb")) + local data = f:read("*a") + f:close() + return vim.fn.sha256(data) + end, + download = function(url, output) + local out = vim.system({ + "curl", + "--output", + output, + url, + "--create-dirs", + }):wait(10000) + return out + end, + delete_file = vim.fn.delete, + ask_user_consent = function(msg, chs, cb) + vim.ui.select(chs, { + prompt = msg, + }, function(choice) + cb(choice) + end) + end, + notify = lib.notify, + detect_existing_version = version_detector.detect_existing_version, + }) + installer.install(config) end, config = config, name = "neotest-java", @@ -141,7 +194,7 @@ local function NeotestJavaAdapter(config, deps) end, }, { __call = function(_, opts, user_deps) - local user_opts = vim.tbl_extend("force", config, opts or {}) + local user_opts = vim.tbl_extend("force", DEFAULT_CONFIG, opts or {}) if type(user_opts.junit_jar) == "string" then user_opts.junit_jar = Path(user_opts.junit_jar) diff --git a/lua/neotest-java/install.lua b/lua/neotest-java/install.lua index a1535b43..25455d45 100644 --- a/lua/neotest-java/install.lua +++ b/lua/neotest-java/install.lua @@ -1,62 +1,144 @@ -local exists = require("neotest.lib.file").exists -local lib = require("neotest.lib") local logger = require("neotest-java.logger") +local version_detector = require("neotest-java.util.junit_version_detector") +local Path = require("neotest-java.model.path") ---- @param file_path neotest-java.Path ---- @return string hash -local checksum = function(file_path) - local f = assert(io.open(file_path:to_string(), "rb")) - local data = f:read("*a") - f:close() - local hash = vim.fn.sha256(data) - return hash -end +---@class neotest-java.InstallDeps +---@field exists fun(filepath: string): boolean +---@field checksum fun(file_path: neotest-java.Path): string +---@field download fun(url: string, output: string): { code: number, stderr: string } +---@field delete_file fun(filepath: string): void +---@field ask_user_consent fun(message: string, choices: string[], callback: fun(choice: string | nil)): void +---@field notify fun(message: string, level?: string): void +---@field detect_existing_version fun(deps?: neotest-java.JunitVersionDetectorDeps): neotest-java.JunitVersion | nil, neotest-java.Path | nil ---- @param config neotest-java.ConfigOpts -local install = function(config) - local filepath = config.junit_jar:to_string() +---@class neotest-java.Installer +---@field install fun(config: neotest-java.ConfigOpts): void - if exists(filepath) then - lib.notify("Already setup!") - return - end - local default_junit_jar_filepath = config.default_junit_jar_filepath:to_string() - - local out = vim.system({ - "curl", - "--output", - default_junit_jar_filepath, - ("https://repo1.maven.org/maven2/org/junit/platform/junit-platform-console-standalone/%s/junit-platform-console-standalone-%s.jar"):format( - config.default_junit_jar_version.version, - config.default_junit_jar_version.version - ), - "--create-dirs", - }):wait(10000) - - if out.code ~= 0 then - lib.notify(string.format("Error while downloading: \n %s", out.stderr), "error") - logger.error(out.stderr) - return - end +--- @param deps neotest-java.InstallDeps +--- @return neotest-java.Installer +local Installer = function(deps) + --- @type neotest-java.InstallDeps + local exists_fn = deps.exists + local checksum_fn = deps.checksum + local delete_file_fn = deps.delete_file + local notify_fn = deps.notify + local detect_existing_version_fn = deps.detect_existing_version + local download_fn = deps.download + local ask_user_consent_fn = deps.ask_user_consent + + --- Download JUnit jar for a specific version + --- @param version_info neotest-java.JunitVersion + --- @param target_filepath string + --- @return boolean success + local function download_junit_jar(version_info, target_filepath) + local url = ("https://repo1.maven.org/maven2/org/junit/platform/junit-platform-console-standalone/%s/junit-platform-console-standalone-%s.jar"):format( + version_info.version, + version_info.version + ) + + local out = download_fn(url, target_filepath) + + if out.code ~= 0 then + notify_fn(string.format("Error while downloading: \n %s", out.stderr), "error") + logger.error(out.stderr) + return false + end + + local sha = checksum_fn(Path(target_filepath)) + local expected_sha = version_info.sha256 + if sha ~= expected_sha then + local message = ([[ + Checksum verification failed! + Expected: %s + Got: %s - local sha = checksum(config.default_junit_jar_filepath) - local expected_sha = config.default_junit_jar_version.sha256 - if sha ~= expected_sha then - local message = ([[ - Checksum verification failed! - Expected: %s - Got: %s + Removed the file at %s. + ]]):format(expected_sha, sha, target_filepath) - Removed the file at %s. - ]]):format(expected_sha, sha, default_junit_jar_filepath) + delete_file_fn(target_filepath) - vim.fn.delete(default_junit_jar_filepath) + notify_fn(message, "error") + logger.error(message) + return false + end - lib.notify(message, "error") - logger.error(message) - return + return true end - lib.notify("Downloaded Junit Standalone successfully at: \n" .. default_junit_jar_filepath) + + return { + --- @param config neotest-java.ConfigOpts + install = function(config) + local filepath = config.junit_jar:to_string() + local default_junit_jar_filepath = config.default_junit_jar_filepath:to_string() + + -- Check if already installed with latest version + if exists_fn(filepath) then + -- Verify it's the correct version by checksum + local current_sha = checksum_fn(Path(filepath)) + if current_sha == config.default_junit_jar_version.sha256 then + notify_fn("JUnit jar is already set up with the latest version!") + return + end + end + + -- Detect existing version + local existing_version, existing_filepath = detect_existing_version_fn() + local has_update, latest_version = false, nil + + if existing_version then + has_update, latest_version = version_detector.check_for_update(existing_version) + end + + -- If there's an existing version and it's not the latest, ask for upgrade + if existing_version and has_update and latest_version then + local message = string.format( + "JUnit jar version %s is installed. A newer version %s is available. Would you like to upgrade?", + existing_version.version, + latest_version.version + ) + ask_user_consent_fn(message, { "Yes, upgrade", "No, keep current version" }, function(choice) + if choice == "Yes, upgrade" then + -- Remove old version + if existing_filepath and exists_fn(existing_filepath:to_string()) then + delete_file_fn(existing_filepath:to_string()) + logger.info("Removed old JUnit jar: " .. existing_filepath:to_string()) + end + + -- Download new version + if download_junit_jar(latest_version, default_junit_jar_filepath) then + notify_fn( + string.format( + "Upgraded JUnit jar from %s to %s successfully at: \n%s", + existing_version.version, + latest_version.version, + default_junit_jar_filepath + ) + ) + end + else + notify_fn("Keeping current JUnit jar version " .. existing_version.version) + end + end) + return + end + + -- If no existing version or user wants fresh install, download latest + if not existing_version or not exists_fn(filepath) then + local message = "JUnit Platform Console Standalone jar is required. Would you like to download it now?" + ask_user_consent_fn(message, { "Yes, download", "No, cancel" }, function(choice) + if choice == "Yes, download" then + if download_junit_jar(config.default_junit_jar_version, default_junit_jar_filepath) then + notify_fn("Downloaded JUnit Standalone successfully at: \n" .. default_junit_jar_filepath) + end + else + notify_fn("Setup cancelled. You can run :NeotestJava setup later to download the JUnit jar.") + end + end) + else + notify_fn("JUnit jar is already set up!") + end + end, + } end -return install +return Installer diff --git a/lua/neotest-java/util/junit_version_detector.lua b/lua/neotest-java/util/junit_version_detector.lua new file mode 100644 index 00000000..d374fb10 --- /dev/null +++ b/lua/neotest-java/util/junit_version_detector.lua @@ -0,0 +1,106 @@ +local Path = require("neotest-java.model.path") +local exists = require("neotest.lib.file").exists + +local DEFAULT_CONFIG = require("neotest-java.default_config") + +--- @param file_path neotest-java.Path +--- @param file_reader? fun(path: string): string +--- @return string hash +local function checksum(file_path, file_reader) + file_reader = file_reader + or function(path) + local f = assert(io.open(path, "rb")) + local data = f:read("*a") + f:close() + return data + end + local data = file_reader(file_path:to_string()) + local hash = vim.fn.sha256(data) + return hash +end + +--- Get supported versions from default_config +--- @return table[] +local function get_supported_versions() + return DEFAULT_CONFIG.get_supported_versions() +end + +---@class neotest-java.JunitVersionDetectorDeps +---@field exists? fun(filepath: neotest-java.Path): boolean +---@field checksum? fun(file_path: neotest-java.Path): string +---@field scan? fun(dir: neotest-java.Path, opts: { search_patterns: string[] }): neotest-java.Path[] +---@field stdpath_data? fun(): string + +--- Detect which version of JUnit jar exists in the data directory +--- @param deps? neotest-java.JunitVersionDetectorDeps +--- @return neotest-java.JunitVersion | nil, neotest-java.Path | nil +-- Returns: version_info, filepath +local function detect_existing_version(deps) + deps = deps or {} + -- Create a wrapper for exists that accepts Path instead of string + local exists_fn = deps.exists or function(path) + return exists(path:to_string()) + end + local checksum_fn = deps.checksum or function(path) + return checksum(path) + end + local scan_fn = deps.scan or require("neotest-java.util.dir_scan") + local stdpath_data_fn = deps.stdpath_data or vim.fn.stdpath + + local supported_versions = get_supported_versions() + local data_dir = Path(stdpath_data_fn("data")):append("neotest-java") + + -- First, try to detect by filename + for _, version_info in ipairs(supported_versions) do + local jar_path = data_dir:append("junit-platform-console-standalone-" .. version_info.version .. ".jar") + if exists_fn(jar_path) then + -- Verify by checksum to be sure + local file_sha = checksum_fn(jar_path) + if file_sha == version_info.sha256 then + return version_info, jar_path + end + end + end + + -- If not found by filename, try to detect by checksum + -- This handles cases where the file might have a different name + local ok, jar_files = pcall(function() + return scan_fn(data_dir, { search_patterns = { "junit-platform-console-standalone-.*%.jar" } }) + end) + + if ok and jar_files then + for _, jar_file in ipairs(jar_files) do + -- jar_file is a Path object + local file_sha = checksum_fn(jar_file) + for _, version_info in ipairs(supported_versions) do + if file_sha == version_info.sha256 then + return version_info, jar_file + end + end + end + end + + return nil, nil +end + +--- Check if there's a newer version available than the one currently installed +--- @param current_version neotest-java.JunitVersion +--- @return boolean, neotest-java.JunitVersion | nil +-- Returns: has_update, latest_version +local function check_for_update(current_version) + local supported_versions = get_supported_versions() + local latest_version = supported_versions[1] -- First is always latest + + if current_version.version ~= latest_version.version then + return true, latest_version + end + + return false, nil +end + +return { + detect_existing_version = detect_existing_version, + check_for_update = check_for_update, + get_supported_versions = get_supported_versions, + _checksum = checksum, -- Exposed for testing +} diff --git a/tests/unit/init_spec.lua b/tests/unit/init_spec.lua index 868df09d..0fe06651 100644 --- a/tests/unit/init_spec.lua +++ b/tests/unit/init_spec.lua @@ -25,4 +25,31 @@ describe("NeotestJava plugin", function() }) eq(nil, adapter.root("some_dir")) end) + + it("should respect disable_update_notifications config option", function() + -- Test that the config option is properly merged + local adapter_with_notifications = require("neotest-java")({ + disable_update_notifications = false, + }, { + root_finder = { + find_root = function() + return nil + end, + }, + }) + + local adapter_without_notifications = require("neotest-java")({ + disable_update_notifications = true, + }, { + root_finder = { + find_root = function() + return nil + end, + }, + }) + + -- Both should have the config set correctly + eq(false, adapter_with_notifications.config.disable_update_notifications) + eq(true, adapter_without_notifications.config.disable_update_notifications) + end) end) diff --git a/tests/unit/install_spec.lua b/tests/unit/install_spec.lua new file mode 100644 index 00000000..35524369 --- /dev/null +++ b/tests/unit/install_spec.lua @@ -0,0 +1,323 @@ +local Installer = require("neotest-java.install") +local Path = require("neotest-java.model.path") +local eq = require("tests.assertions").eq + +describe("Installer", function() + local version_6_0_1 = { + version = "6.0.1", + sha256 = "3009120b7953bfe63add272e65b2bbeca0d41d0dfd8dea605201db15b640e0ff", + } + local version_1_10_1 = { + version = "1.10.1", + sha256 = "b42eaa53d13576d17db5fb8b280722a6ae9e36daf95f4262bc6e96d4cb20725f", + } + + local default_config = { + junit_jar = Path("/data/junit-6.0.1.jar"), + default_junit_jar_filepath = Path("/data/junit-6.0.1.jar"), + default_junit_jar_version = version_6_0_1, + } + + it("should notify when already set up with latest version", function() + local notifications = {} + local exists_fn = function(filepath) + return filepath == "/data/junit-6.0.1.jar" + end + + local checksum_fn = function(_file_path) + return version_6_0_1.sha256 + end + + local deps = { + exists = exists_fn, + checksum = checksum_fn, + notify = function(message, level) + table.insert(notifications, { message = message, level = level }) + end, + detect_existing_version = function() + return nil, nil + end, + } + + local installer = Installer(deps) + installer.install(default_config) + + eq(1, #notifications) + eq("JUnit jar is already set up with the latest version!", notifications[1].message) + end) + + it("should ask for upgrade when older version is detected", function() + local notifications = {} + local user_choices = {} + local downloads = {} + + local exists_fn = function(filepath) + -- Return true for old version, false for new version (not downloaded yet) + return filepath == "/data/junit-1.10.1.jar" + end + + local checksum_fn = function(file_path) + local path_str = file_path:to_string() + -- Return checksum based on file path + if path_str:match("1%.10%.1") then + return version_1_10_1.sha256 + elseif path_str:match("6%.0%.1") then + return version_6_0_1.sha256 + end + return version_6_0_1.sha256 + end + + local ask_user_consent_fn = function(message, choices, callback) + table.insert(user_choices, { message = message, choices = choices }) + -- Simulate user choosing "Yes, upgrade" + callback("Yes, upgrade") + end + + local download_fn = function(url, output) + table.insert(downloads, { url = url, output = output }) + return { code = 0, stderr = "" } + end + + local delete_file_fn = function(_filepath) + -- Mock delete + end + + local deps = { + exists = exists_fn, + checksum = checksum_fn, + notify = function(message, level) + table.insert(notifications, { message = message, level = level }) + end, + detect_existing_version = function() + return version_1_10_1, Path("/data/junit-1.10.1.jar") + end, + ask_user_consent = ask_user_consent_fn, + download = download_fn, + delete_file = delete_file_fn, + } + + local installer = Installer(deps) + installer.install(default_config) + + -- Should have asked for upgrade + eq(1, #user_choices) + assert(user_choices[1].message:match("upgrade"), "Should ask about upgrade") + + -- Should have downloaded new version + eq(1, #downloads) + assert(downloads[1].url:match("6%.0%.1"), "Should download version 6.0.1") + + -- Should have notified about upgrade + local upgrade_notification = false + for _, notif in ipairs(notifications) do + if notif.message:match("Upgraded") then + upgrade_notification = true + break + end + end + assert(upgrade_notification, "Should notify about upgrade") + end) + + it("should keep current version when user declines upgrade", function() + local notifications = {} + local user_choices = {} + + local exists_fn = function(filepath) + return filepath == "/data/junit-1.10.1.jar" + end + + local checksum_fn = function(_file_path) + return version_1_10_1.sha256 + end + + local ask_user_consent_fn = function(message, choices, callback) + table.insert(user_choices, { message = message, choices = choices }) + -- Simulate user choosing "No, keep current version" + callback("No, keep current version") + end + + local deps = { + exists = exists_fn, + checksum = checksum_fn, + notify = function(message, level) + table.insert(notifications, { message = message, level = level }) + end, + detect_existing_version = function() + return version_1_10_1, Path("/data/junit-1.10.1.jar") + end, + ask_user_consent = ask_user_consent_fn, + } + + local installer = Installer(deps) + installer.install(default_config) + + -- Should have asked for upgrade + eq(1, #user_choices) + + -- Should have notified about keeping current version + local keep_notification = false + for _, notif in ipairs(notifications) do + if notif.message:match("Keeping current") then + keep_notification = true + break + end + end + assert(keep_notification, "Should notify about keeping current version") + end) + + it("should ask for download when no version exists", function() + local notifications = {} + local user_choices = {} + local downloads = {} + + local exists_fn = function() + return false + end + + local ask_user_consent_fn = function(message, choices, callback) + table.insert(user_choices, { message = message, choices = choices }) + -- Simulate user choosing "Yes, download" + callback("Yes, download") + end + + local download_fn = function(url, output) + table.insert(downloads, { url = url, output = output }) + return { code = 0, stderr = "" } + end + + local checksum_fn = function(_file_path) + return version_6_0_1.sha256 + end + + local deps = { + exists = exists_fn, + checksum = checksum_fn, + notify = function(message, level) + table.insert(notifications, { message = message, level = level }) + end, + detect_existing_version = function() + return nil, nil + end, + ask_user_consent = ask_user_consent_fn, + download = download_fn, + } + + local installer = Installer(deps) + installer.install(default_config) + + -- Should have asked for download + eq(1, #user_choices) + assert(user_choices[1].message:match("download"), "Should ask about download") + + -- Should have downloaded + eq(1, #downloads) + assert(downloads[1].url:match("6%.0%.1"), "Should download version 6.0.1") + + -- Should have notified about download + local download_notification = false + for _, notif in ipairs(notifications) do + if notif.message:match("Downloaded") then + download_notification = true + break + end + end + assert(download_notification, "Should notify about download") + end) + + it("should handle download error", function() + local notifications = {} + + local exists_fn = function() + return false + end + + local ask_user_consent_fn = function(_message, _choices, callback) + callback("Yes, download") + end + + local download_fn = function(_url, _output) + return { code = 1, stderr = "Network error" } + end + + local deps = { + exists = exists_fn, + notify = function(message, level) + table.insert(notifications, { message = message, level = level }) + end, + detect_existing_version = function() + return nil, nil + end, + ask_user_consent = ask_user_consent_fn, + download = download_fn, + } + + local installer = Installer(deps) + installer.install(default_config) + + -- Should have error notification + local error_notification = false + for _, notif in ipairs(notifications) do + if notif.level == "error" or notif.message:match("Error") then + error_notification = true + break + end + end + assert(error_notification, "Should notify about error") + end) + + it("should handle checksum verification failure", function() + local notifications = {} + local deleted_files = {} + + local exists_fn = function() + return false + end + + local ask_user_consent_fn = function(_message, _choices, callback) + callback("Yes, download") + end + + local download_fn = function(_url, _output) + return { code = 0, stderr = "" } + end + + local checksum_fn = function(_file_path) + -- Return wrong checksum + return "wrong_checksum" + end + + local delete_file_fn = function(filepath) + table.insert(deleted_files, filepath) + end + + local deps = { + exists = exists_fn, + checksum = checksum_fn, + notify = function(message, level) + table.insert(notifications, { message = message, level = level }) + end, + detect_existing_version = function() + return nil, nil + end, + ask_user_consent = ask_user_consent_fn, + download = download_fn, + delete_file = delete_file_fn, + } + + local installer = Installer(deps) + installer.install(default_config) + + -- Should have deleted the file + eq(1, #deleted_files) + + -- Should have error notification about checksum + local checksum_error = false + for _, notif in ipairs(notifications) do + if notif.message:match("Checksum") then + checksum_error = true + break + end + end + assert(checksum_error, "Should notify about checksum error") + end) +end) diff --git a/tests/unit/junit_version_detector_spec.lua b/tests/unit/junit_version_detector_spec.lua new file mode 100644 index 00000000..ce33f91d --- /dev/null +++ b/tests/unit/junit_version_detector_spec.lua @@ -0,0 +1,149 @@ +local version_detector = require("neotest-java.util.junit_version_detector") +local Path = require("neotest-java.model.path") +local eq = require("tests.assertions").eq + +describe("JUnit Version Detector", function() + it("should detect existing version by filename", function() + local version_6_0_1 = { + version = "6.0.1", + sha256 = "3009120b7953bfe63add272e65b2bbeca0d41d0dfd8dea605201db15b640e0ff", + } + local version_1_10_1 = { + version = "1.10.1", + sha256 = "b42eaa53d13576d17db5fb8b280722a6ae9e36daf95f4262bc6e96d4cb20725f", + } + + -- Use Path to construct expected path (works on both Windows and Unix) + local data_dir_str = "data" + -- Build the path exactly the same way the detector does + local data_dir = Path(data_dir_str):append("neotest-java") + local expected_jar_path = data_dir:append("junit-platform-console-standalone-6.0.1.jar") + + local exists_fn = function(filepath) + if filepath ~= expected_jar_path then + return false + end + return true + end + + local checksum_fn = function(file_path) + local path_str = file_path:to_string() + -- Mock checksum based on path + if path_str:match("6%.0%.1") then + return version_6_0_1.sha256 + elseif path_str:match("1%.10%.1") then + return version_1_10_1.sha256 + end + return "unknown" + end + + local deps = { + exists = exists_fn, + checksum = checksum_fn, + stdpath_data = function() + return data_dir_str + end, + } + + local detected_version, filepath = version_detector.detect_existing_version(deps) + + assert(detected_version ~= nil, "detected_version should not be nil") + eq(version_6_0_1.version, detected_version.version) + eq(version_6_0_1.sha256, detected_version.sha256) + -- Compare using Path to handle different path formats (Windows vs Unix) + eq(expected_jar_path, filepath) + end) + + it("should return nil when no version is found", function() + local exists_fn = function(_filepath) + return false + end + + local scan_fn = function() + return {} + end + + local deps = { + exists = exists_fn, + scan = scan_fn, + stdpath_data = function() + return "data" + end, + } + + local detected_version, filepath = version_detector.detect_existing_version(deps) + + eq(nil, detected_version) + eq(nil, filepath) + end) + + it("should detect version by checksum when filename doesn't match", function() + local version_1_10_1 = { + version = "1.10.1", + sha256 = "b42eaa53d13576d17db5fb8b280722a6ae9e36daf95f4262bc6e96d4cb20725f", + } + + local data_dir_str = "data" + local data_dir = Path(data_dir_str):append("neotest-java") + local jar_file = data_dir:append("custom-junit.jar") + local jar_file_str = jar_file:to_string() + + local exists_fn = function(_filepath) + return false -- No file with expected filename + end + + local scan_fn = function() + return { jar_file } + end + + local checksum_fn = function(file_path) + -- Compare using Path to handle different path formats (Windows vs Unix) + local file_path_str = Path(file_path:to_string()):to_string() + if file_path_str == jar_file_str then + return version_1_10_1.sha256 + end + return "unknown" + end + + local deps = { + exists = exists_fn, + scan = scan_fn, + checksum = checksum_fn, + stdpath_data = function() + return data_dir_str + end, + } + + local detected_version, filepath = version_detector.detect_existing_version(deps) + + assert(detected_version ~= nil, "detected_version should not be nil") + eq(version_1_10_1.version, detected_version.version) + eq(version_1_10_1.sha256, detected_version.sha256) + -- Compare using Path to handle different path formats (Windows vs Unix) + eq(jar_file, filepath) + end) + + it("should check for update when current version is older", function() + local current_version = { + version = "1.10.1", + sha256 = "b42eaa53d13576d17db5fb8b280722a6ae9e36daf95f4262bc6e96d4cb20725f", + } + + local has_update, latest_version = version_detector.check_for_update(current_version) + + eq(true, has_update) + eq("6.0.1", latest_version.version) + end) + + it("should not find update when current version is latest", function() + local current_version = { + version = "6.0.1", + sha256 = "3009120b7953bfe63add272e65b2bbeca0d41d0dfd8dea605201db15b640e0ff", + } + + local has_update, latest_version = version_detector.check_for_update(current_version) + + eq(false, has_update) + eq(nil, latest_version) + end) +end) From 0ae7e2d0d33d889db7a83410ffffde111431f72b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Cas=C3=ADa?= <31012661+rcasia@users.noreply.github.com> Date: Tue, 20 Jan 2026 23:35:04 +0100 Subject: [PATCH 3/6] fix: make windows compatible --- lua/neotest-java/install.lua | 30 +++++++++++++++++------------- tests/unit/install_spec.lua | 36 +++++++++++++++++++++++++++++++++--- 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/lua/neotest-java/install.lua b/lua/neotest-java/install.lua index 25455d45..c27a1f04 100644 --- a/lua/neotest-java/install.lua +++ b/lua/neotest-java/install.lua @@ -1,6 +1,5 @@ local logger = require("neotest-java.logger") local version_detector = require("neotest-java.util.junit_version_detector") -local Path = require("neotest-java.model.path") ---@class neotest-java.InstallDeps ---@field exists fun(filepath: string): boolean @@ -28,7 +27,7 @@ local Installer = function(deps) --- Download JUnit jar for a specific version --- @param version_info neotest-java.JunitVersion - --- @param target_filepath string + --- @param target_filepath neotest-java.Path --- @return boolean success local function download_junit_jar(version_info, target_filepath) local url = ("https://repo1.maven.org/maven2/org/junit/platform/junit-platform-console-standalone/%s/junit-platform-console-standalone-%s.jar"):format( @@ -36,7 +35,8 @@ local Installer = function(deps) version_info.version ) - local out = download_fn(url, target_filepath) + local target_filepath_str = target_filepath:to_string() + local out = download_fn(url, target_filepath_str) if out.code ~= 0 then notify_fn(string.format("Error while downloading: \n %s", out.stderr), "error") @@ -44,7 +44,7 @@ local Installer = function(deps) return false end - local sha = checksum_fn(Path(target_filepath)) + local sha = checksum_fn(target_filepath) local expected_sha = version_info.sha256 if sha ~= expected_sha then local message = ([[ @@ -53,9 +53,9 @@ local Installer = function(deps) Got: %s Removed the file at %s. - ]]):format(expected_sha, sha, target_filepath) + ]]):format(expected_sha, sha, target_filepath_str) - delete_file_fn(target_filepath) + delete_file_fn(target_filepath_str) notify_fn(message, "error") logger.error(message) @@ -68,13 +68,14 @@ local Installer = function(deps) return { --- @param config neotest-java.ConfigOpts install = function(config) - local filepath = config.junit_jar:to_string() - local default_junit_jar_filepath = config.default_junit_jar_filepath:to_string() + local filepath = config.junit_jar + local default_junit_jar_filepath = config.default_junit_jar_filepath + local filepath_str = filepath:to_string() -- Check if already installed with latest version - if exists_fn(filepath) then + if exists_fn(filepath_str) then -- Verify it's the correct version by checksum - local current_sha = checksum_fn(Path(filepath)) + local current_sha = checksum_fn(filepath) if current_sha == config.default_junit_jar_version.sha256 then notify_fn("JUnit jar is already set up with the latest version!") return @@ -111,7 +112,7 @@ local Installer = function(deps) "Upgraded JUnit jar from %s to %s successfully at: \n%s", existing_version.version, latest_version.version, - default_junit_jar_filepath + default_junit_jar_filepath:to_string() ) ) end @@ -123,12 +124,15 @@ local Installer = function(deps) end -- If no existing version or user wants fresh install, download latest - if not existing_version or not exists_fn(filepath) then + if not existing_version or not exists_fn(filepath_str) then local message = "JUnit Platform Console Standalone jar is required. Would you like to download it now?" ask_user_consent_fn(message, { "Yes, download", "No, cancel" }, function(choice) if choice == "Yes, download" then if download_junit_jar(config.default_junit_jar_version, default_junit_jar_filepath) then - notify_fn("Downloaded JUnit Standalone successfully at: \n" .. default_junit_jar_filepath) + notify_fn( + "Downloaded JUnit Standalone successfully at: \n" + .. default_junit_jar_filepath:to_string() + ) end else notify_fn("Setup cancelled. You can run :NeotestJava setup later to download the JUnit jar.") diff --git a/tests/unit/install_spec.lua b/tests/unit/install_spec.lua index 35524369..59de986a 100644 --- a/tests/unit/install_spec.lua +++ b/tests/unit/install_spec.lua @@ -20,8 +20,10 @@ describe("Installer", function() it("should notify when already set up with latest version", function() local notifications = {} + local expected_path = Path("/data/junit-6.0.1.jar") local exists_fn = function(filepath) - return filepath == "/data/junit-6.0.1.jar" + -- Compare paths using Path to handle Windows/Unix differences + return Path(filepath) == expected_path end local checksum_fn = function(_file_path) @@ -37,6 +39,15 @@ describe("Installer", function() detect_existing_version = function() return nil, nil end, + ask_user_consent = function(_message, _choices, callback) + callback("No, cancel") + end, + download = function(_url, _output) + return { code = 0, stderr = "" } + end, + delete_file = function(_filepath) + -- Mock delete + end, } local installer = Installer(deps) @@ -51,9 +62,11 @@ describe("Installer", function() local user_choices = {} local downloads = {} + local expected_old_path = Path("/data/junit-1.10.1.jar") local exists_fn = function(filepath) -- Return true for old version, false for new version (not downloaded yet) - return filepath == "/data/junit-1.10.1.jar" + -- Compare paths using Path to handle Windows/Unix differences + return Path(filepath) == expected_old_path end local checksum_fn = function(file_path) @@ -122,8 +135,10 @@ describe("Installer", function() local notifications = {} local user_choices = {} + local expected_old_path = Path("/data/junit-1.10.1.jar") local exists_fn = function(filepath) - return filepath == "/data/junit-1.10.1.jar" + -- Compare paths using Path to handle Windows/Unix differences + return Path(filepath) == expected_old_path end local checksum_fn = function(_file_path) @@ -146,6 +161,12 @@ describe("Installer", function() return version_1_10_1, Path("/data/junit-1.10.1.jar") end, ask_user_consent = ask_user_consent_fn, + download = function(_url, _output) + return { code = 0, stderr = "" } + end, + delete_file = function(_filepath) + -- Mock delete + end, } local installer = Installer(deps) @@ -200,6 +221,9 @@ describe("Installer", function() end, ask_user_consent = ask_user_consent_fn, download = download_fn, + delete_file = function(_filepath) + -- Mock delete + end, } local installer = Installer(deps) @@ -249,6 +273,12 @@ describe("Installer", function() end, ask_user_consent = ask_user_consent_fn, download = download_fn, + checksum = function(_file_path) + return version_6_0_1.sha256 + end, + delete_file = function(_filepath) + -- Mock delete + end, } local installer = Installer(deps) From da9196f9b52099273a205d7054f11431a74f50c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Cas=C3=ADa?= <31012661+rcasia@users.noreply.github.com> Date: Tue, 20 Jan 2026 23:49:44 +0100 Subject: [PATCH 4/6] refactor: make junit version detector a constructor method --- lua/neotest-java/init.lua | 3 +- lua/neotest-java/install.lua | 5 +- .../util/junit_version_detector.lua | 116 ++++++++++-------- tests/unit/junit_version_detector_spec.lua | 27 ++-- 4 files changed, 86 insertions(+), 65 deletions(-) diff --git a/lua/neotest-java/init.lua b/lua/neotest-java/init.lua index 28d64553..773ee7f8 100644 --- a/lua/neotest-java/init.lua +++ b/lua/neotest-java/init.lua @@ -12,7 +12,8 @@ local Path = require("neotest-java.model.path") local nio = require("nio") local logger = require("neotest-java.logger") local Binaries = require("neotest-java.command.binaries") -local version_detector = require("neotest-java.util.junit_version_detector") +local JunitVersionDetector = require("neotest-java.util.junit_version_detector") +local version_detector = JunitVersionDetector() local lib = require("neotest.lib") local exists = require("neotest.lib.file").exists diff --git a/lua/neotest-java/install.lua b/lua/neotest-java/install.lua index c27a1f04..c93df08c 100644 --- a/lua/neotest-java/install.lua +++ b/lua/neotest-java/install.lua @@ -1,5 +1,6 @@ local logger = require("neotest-java.logger") -local version_detector = require("neotest-java.util.junit_version_detector") +local JunitVersionDetector = require("neotest-java.util.junit_version_detector") +local version_detector = JunitVersionDetector() ---@class neotest-java.InstallDeps ---@field exists fun(filepath: string): boolean @@ -8,7 +9,7 @@ local version_detector = require("neotest-java.util.junit_version_detector") ---@field delete_file fun(filepath: string): void ---@field ask_user_consent fun(message: string, choices: string[], callback: fun(choice: string | nil)): void ---@field notify fun(message: string, level?: string): void ----@field detect_existing_version fun(deps?: neotest-java.JunitVersionDetectorDeps): neotest-java.JunitVersion | nil, neotest-java.Path | nil +---@field detect_existing_version fun(): neotest-java.JunitVersion | nil, neotest-java.Path | nil ---@class neotest-java.Installer ---@field install fun(config: neotest-java.ConfigOpts): void diff --git a/lua/neotest-java/util/junit_version_detector.lua b/lua/neotest-java/util/junit_version_detector.lua index d374fb10..bfb54b80 100644 --- a/lua/neotest-java/util/junit_version_detector.lua +++ b/lua/neotest-java/util/junit_version_detector.lua @@ -31,12 +31,17 @@ end ---@field scan? fun(dir: neotest-java.Path, opts: { search_patterns: string[] }): neotest-java.Path[] ---@field stdpath_data? fun(): string ---- Detect which version of JUnit jar exists in the data directory +---@class neotest-java.JunitVersionDetector +---@field detect_existing_version fun(): neotest-java.JunitVersion | nil, neotest-java.Path | nil +---@field check_for_update fun(current_version: neotest-java.JunitVersion): boolean, neotest-java.JunitVersion | nil +---@field get_supported_versions fun(): table[] +---@field _checksum fun(file_path: neotest-java.Path, file_reader?: fun(path: string): string): string + --- @param deps? neotest-java.JunitVersionDetectorDeps ---- @return neotest-java.JunitVersion | nil, neotest-java.Path | nil --- Returns: version_info, filepath -local function detect_existing_version(deps) +--- @return neotest-java.JunitVersionDetector +local JunitVersionDetector = function(deps) deps = deps or {} + -- Create a wrapper for exists that accepts Path instead of string local exists_fn = deps.exists or function(path) return exists(path:to_string()) @@ -47,60 +52,71 @@ local function detect_existing_version(deps) local scan_fn = deps.scan or require("neotest-java.util.dir_scan") local stdpath_data_fn = deps.stdpath_data or vim.fn.stdpath - local supported_versions = get_supported_versions() - local data_dir = Path(stdpath_data_fn("data")):append("neotest-java") - - -- First, try to detect by filename - for _, version_info in ipairs(supported_versions) do - local jar_path = data_dir:append("junit-platform-console-standalone-" .. version_info.version .. ".jar") - if exists_fn(jar_path) then - -- Verify by checksum to be sure - local file_sha = checksum_fn(jar_path) - if file_sha == version_info.sha256 then - return version_info, jar_path + return { + --- Detect which version of JUnit jar exists in the data directory + --- @return neotest-java.JunitVersion | nil, neotest-java.Path | nil + -- Returns: version_info, filepath + detect_existing_version = function() + local supported_versions = get_supported_versions() + local data_dir = Path(stdpath_data_fn("data")):append("neotest-java") + + -- First, try to detect by filename + for _, version_info in ipairs(supported_versions) do + local jar_path = data_dir:append("junit-platform-console-standalone-" .. version_info.version .. ".jar") + if exists_fn(jar_path) then + -- Verify by checksum to be sure + local file_sha = checksum_fn(jar_path) + if file_sha == version_info.sha256 then + return version_info, jar_path + end + end end - end - end - -- If not found by filename, try to detect by checksum - -- This handles cases where the file might have a different name - local ok, jar_files = pcall(function() - return scan_fn(data_dir, { search_patterns = { "junit-platform-console-standalone-.*%.jar" } }) - end) + -- If not found by filename, try to detect by checksum + -- This handles cases where the file might have a different name + local ok, jar_files = pcall(function() + return scan_fn(data_dir, { search_patterns = { "junit-platform-console-standalone-.*%.jar" } }) + end) - if ok and jar_files then - for _, jar_file in ipairs(jar_files) do - -- jar_file is a Path object - local file_sha = checksum_fn(jar_file) - for _, version_info in ipairs(supported_versions) do - if file_sha == version_info.sha256 then - return version_info, jar_file + if ok and jar_files then + for _, jar_file in ipairs(jar_files) do + -- jar_file is a Path object + local file_sha = checksum_fn(jar_file) + for _, version_info in ipairs(supported_versions) do + if file_sha == version_info.sha256 then + return version_info, jar_file + end + end end end - end - end - return nil, nil -end + return nil, nil + end, ---- Check if there's a newer version available than the one currently installed ---- @param current_version neotest-java.JunitVersion ---- @return boolean, neotest-java.JunitVersion | nil --- Returns: has_update, latest_version -local function check_for_update(current_version) - local supported_versions = get_supported_versions() - local latest_version = supported_versions[1] -- First is always latest + --- Check if there's a newer version available than the one currently installed + --- @param current_version neotest-java.JunitVersion + --- @return boolean, neotest-java.JunitVersion | nil + -- Returns: has_update, latest_version + check_for_update = function(current_version) + local supported_versions = get_supported_versions() + local latest_version = supported_versions[1] -- First is always latest - if current_version.version ~= latest_version.version then - return true, latest_version - end + if current_version.version ~= latest_version.version then + return true, latest_version + end + + return false, nil + end, + + --- Get supported versions from default_config + --- @return table[] + get_supported_versions = get_supported_versions, - return false, nil + --- @param file_path neotest-java.Path + --- @param file_reader? fun(path: string): string + --- @return string hash + _checksum = checksum, -- Exposed for testing + } end -return { - detect_existing_version = detect_existing_version, - check_for_update = check_for_update, - get_supported_versions = get_supported_versions, - _checksum = checksum, -- Exposed for testing -} +return JunitVersionDetector diff --git a/tests/unit/junit_version_detector_spec.lua b/tests/unit/junit_version_detector_spec.lua index ce33f91d..757bba01 100644 --- a/tests/unit/junit_version_detector_spec.lua +++ b/tests/unit/junit_version_detector_spec.lua @@ -1,8 +1,9 @@ -local version_detector = require("neotest-java.util.junit_version_detector") +local JunitVersionDetector = require("neotest-java.util.junit_version_detector") local Path = require("neotest-java.model.path") local eq = require("tests.assertions").eq describe("JUnit Version Detector", function() + local version_detector = JunitVersionDetector() it("should detect existing version by filename", function() local version_6_0_1 = { version = "6.0.1", @@ -37,15 +38,15 @@ describe("JUnit Version Detector", function() return "unknown" end - local deps = { + local detector = JunitVersionDetector({ exists = exists_fn, checksum = checksum_fn, stdpath_data = function() return data_dir_str end, - } + }) - local detected_version, filepath = version_detector.detect_existing_version(deps) + local detected_version, filepath = detector.detect_existing_version() assert(detected_version ~= nil, "detected_version should not be nil") eq(version_6_0_1.version, detected_version.version) @@ -63,15 +64,15 @@ describe("JUnit Version Detector", function() return {} end - local deps = { + local detector = JunitVersionDetector({ exists = exists_fn, scan = scan_fn, stdpath_data = function() return "data" end, - } + }) - local detected_version, filepath = version_detector.detect_existing_version(deps) + local detected_version, filepath = detector.detect_existing_version() eq(nil, detected_version) eq(nil, filepath) @@ -105,16 +106,16 @@ describe("JUnit Version Detector", function() return "unknown" end - local deps = { + local detector = JunitVersionDetector({ exists = exists_fn, scan = scan_fn, checksum = checksum_fn, stdpath_data = function() return data_dir_str end, - } + }) - local detected_version, filepath = version_detector.detect_existing_version(deps) + local detected_version, filepath = detector.detect_existing_version() assert(detected_version ~= nil, "detected_version should not be nil") eq(version_1_10_1.version, detected_version.version) @@ -124,24 +125,26 @@ describe("JUnit Version Detector", function() end) it("should check for update when current version is older", function() + local detector = JunitVersionDetector() local current_version = { version = "1.10.1", sha256 = "b42eaa53d13576d17db5fb8b280722a6ae9e36daf95f4262bc6e96d4cb20725f", } - local has_update, latest_version = version_detector.check_for_update(current_version) + local has_update, latest_version = detector.check_for_update(current_version) eq(true, has_update) eq("6.0.1", latest_version.version) end) it("should not find update when current version is latest", function() + local detector = JunitVersionDetector() local current_version = { version = "6.0.1", sha256 = "3009120b7953bfe63add272e65b2bbeca0d41d0dfd8dea605201db15b640e0ff", } - local has_update, latest_version = version_detector.check_for_update(current_version) + local has_update, latest_version = detector.check_for_update(current_version) eq(false, has_update) eq(nil, latest_version) From 6ef7cb2a12025619c3f52d61aac5422a3d0a457a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Cas=C3=ADa?= <31012661+rcasia@users.noreply.github.com> Date: Wed, 21 Jan 2026 00:06:01 +0100 Subject: [PATCH 5/6] refactor: enhance JUnit Version Detector with dependency injection for better configurability --- lua/neotest-java/init.lua | 14 ++++++++- lua/neotest-java/install.lua | 15 ++++++++- .../util/junit_version_detector.lua | 26 ++++++---------- tests/unit/junit_version_detector_spec.lua | 31 +++++++++++++++++-- 4 files changed, 64 insertions(+), 22 deletions(-) diff --git a/lua/neotest-java/init.lua b/lua/neotest-java/init.lua index 773ee7f8..c19f9e65 100644 --- a/lua/neotest-java/init.lua +++ b/lua/neotest-java/init.lua @@ -13,7 +13,19 @@ local nio = require("nio") local logger = require("neotest-java.logger") local Binaries = require("neotest-java.command.binaries") local JunitVersionDetector = require("neotest-java.util.junit_version_detector") -local version_detector = JunitVersionDetector() +local version_detector = JunitVersionDetector({ + exists = function(path) + return File.exists(path:to_string()) + end, + checksum = function(path) + local f = assert(io.open(path:to_string(), "rb")) + local data = f:read("*a") + f:close() + return vim.fn.sha256(data) + end, + scan = require("neotest-java.util.dir_scan"), + stdpath_data = vim.fn.stdpath, +}) local lib = require("neotest.lib") local exists = require("neotest.lib.file").exists diff --git a/lua/neotest-java/install.lua b/lua/neotest-java/install.lua index c93df08c..9bd63e31 100644 --- a/lua/neotest-java/install.lua +++ b/lua/neotest-java/install.lua @@ -1,6 +1,19 @@ local logger = require("neotest-java.logger") local JunitVersionDetector = require("neotest-java.util.junit_version_detector") -local version_detector = JunitVersionDetector() +local exists = require("neotest.lib.file").exists +local version_detector = JunitVersionDetector({ + exists = function(path) + return exists(path:to_string()) + end, + checksum = function(path) + local f = assert(io.open(path:to_string(), "rb")) + local data = f:read("*a") + f:close() + return vim.fn.sha256(data) + end, + scan = require("neotest-java.util.dir_scan"), + stdpath_data = vim.fn.stdpath, +}) ---@class neotest-java.InstallDeps ---@field exists fun(filepath: string): boolean diff --git a/lua/neotest-java/util/junit_version_detector.lua b/lua/neotest-java/util/junit_version_detector.lua index bfb54b80..300b02a3 100644 --- a/lua/neotest-java/util/junit_version_detector.lua +++ b/lua/neotest-java/util/junit_version_detector.lua @@ -1,5 +1,4 @@ local Path = require("neotest-java.model.path") -local exists = require("neotest.lib.file").exists local DEFAULT_CONFIG = require("neotest-java.default_config") @@ -26,10 +25,10 @@ local function get_supported_versions() end ---@class neotest-java.JunitVersionDetectorDeps ----@field exists? fun(filepath: neotest-java.Path): boolean ----@field checksum? fun(file_path: neotest-java.Path): string ----@field scan? fun(dir: neotest-java.Path, opts: { search_patterns: string[] }): neotest-java.Path[] ----@field stdpath_data? fun(): string +---@field exists fun(filepath: neotest-java.Path): boolean +---@field checksum fun(file_path: neotest-java.Path): string +---@field scan fun(dir: neotest-java.Path, opts: { search_patterns: string[] }): neotest-java.Path[] +---@field stdpath_data fun(): string ---@class neotest-java.JunitVersionDetector ---@field detect_existing_version fun(): neotest-java.JunitVersion | nil, neotest-java.Path | nil @@ -37,20 +36,13 @@ end ---@field get_supported_versions fun(): table[] ---@field _checksum fun(file_path: neotest-java.Path, file_reader?: fun(path: string): string): string ---- @param deps? neotest-java.JunitVersionDetectorDeps +--- @param deps neotest-java.JunitVersionDetectorDeps --- @return neotest-java.JunitVersionDetector local JunitVersionDetector = function(deps) - deps = deps or {} - - -- Create a wrapper for exists that accepts Path instead of string - local exists_fn = deps.exists or function(path) - return exists(path:to_string()) - end - local checksum_fn = deps.checksum or function(path) - return checksum(path) - end - local scan_fn = deps.scan or require("neotest-java.util.dir_scan") - local stdpath_data_fn = deps.stdpath_data or vim.fn.stdpath + local exists_fn = deps.exists + local checksum_fn = deps.checksum + local scan_fn = deps.scan + local stdpath_data_fn = deps.stdpath_data return { --- Detect which version of JUnit jar exists in the data directory diff --git a/tests/unit/junit_version_detector_spec.lua b/tests/unit/junit_version_detector_spec.lua index 757bba01..92a2982a 100644 --- a/tests/unit/junit_version_detector_spec.lua +++ b/tests/unit/junit_version_detector_spec.lua @@ -3,7 +3,6 @@ local Path = require("neotest-java.model.path") local eq = require("tests.assertions").eq describe("JUnit Version Detector", function() - local version_detector = JunitVersionDetector() it("should detect existing version by filename", function() local version_6_0_1 = { version = "6.0.1", @@ -125,7 +124,20 @@ describe("JUnit Version Detector", function() end) it("should check for update when current version is older", function() - local detector = JunitVersionDetector() + local detector = JunitVersionDetector({ + exists = function(_path) + return false + end, + checksum = function(_path) + return "dummy" + end, + scan = function() + return {} + end, + stdpath_data = function() + return "data" + end, + }) local current_version = { version = "1.10.1", sha256 = "b42eaa53d13576d17db5fb8b280722a6ae9e36daf95f4262bc6e96d4cb20725f", @@ -138,7 +150,20 @@ describe("JUnit Version Detector", function() end) it("should not find update when current version is latest", function() - local detector = JunitVersionDetector() + local detector = JunitVersionDetector({ + exists = function(_path) + return false + end, + checksum = function(_path) + return "dummy" + end, + scan = function() + return {} + end, + stdpath_data = function() + return "data" + end, + }) local current_version = { version = "6.0.1", sha256 = "3009120b7953bfe63add272e65b2bbeca0d41d0dfd8dea605201db15b640e0ff", From 7eca04562b9993f14d72cf2a630bdcd78aa64b52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ricardo=20Cas=C3=ADa?= <31012661+rcasia@users.noreply.github.com> Date: Wed, 21 Jan 2026 18:22:50 +0100 Subject: [PATCH 6/6] WIP --- lua/neotest-java/init.lua | 67 ++++++++++++++++++++++++++++--------- tests/unit/init_spec.lua | 70 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+), 15 deletions(-) diff --git a/lua/neotest-java/init.lua b/lua/neotest-java/init.lua index c19f9e65..d3370999 100644 --- a/lua/neotest-java/init.lua +++ b/lua/neotest-java/init.lua @@ -40,18 +40,6 @@ local build_tools = require("neotest-java.build_tool") local detect_project_type = require("neotest-java.util.detect_project_type") local compilers = require("neotest-java.core.spec_builder.compiler") ---- @param filepath neotest-java.Path -local check_junit_jar = function(filepath, default_version) - local _exists, _ = File.exists(filepath:to_string()) - assert( - _exists, - ([[ - Junit Platform Console Standalone jar not found at %s - Please run the following command to download it: NeotestJava setup - Or alternatively, download it from https://repo1.maven.org/maven2/org/junit/platform/junit-platform-console-standalone/%s/junit-platform-console-standalone-%s.jar - ]]):format(filepath, default_version, default_version) - ) -end local mkdir = function(dir) vim.uv.fs_mkdir(dir:to_string(), 493) @@ -66,8 +54,13 @@ end --- @field install fun() --- +---@class neotest-java.CheckJunitJarDeps +---@field file_exists? fun(filepath: string): boolean +---@field version_detector? neotest-java.JunitVersionDetector + --- @class neotest-java.Dependencies ---- @field root_finder { find_root: fun(dir: string): string | nil } +---@field root_finder? { find_root: fun(dir: string): string | nil } +---@field check_junit_jar_deps? neotest-java.CheckJunitJarDeps --- @param config neotest-java.ConfigOpts --- @param deps? neotest-java.Dependencies @@ -76,6 +69,7 @@ local function NeotestJavaAdapter(config, deps) config = vim.tbl_extend("force", DEFAULT_CONFIG, config or {}) deps = deps or {} local _root_finder = deps and deps.root_finder or root_finder + local check_junit_jar_deps = deps.check_junit_jar_deps or {} log.info("neotest-java adapter initialized") @@ -84,6 +78,46 @@ local function NeotestJavaAdapter(config, deps) -- create data directory if it doesn't exist mkdir(Path(vim.fn.stdpath("data")):append("neotest-java")) + -- Local function to check JUnit jar with dependencies from constructor + --- @param filepath neotest-java.Path + --- @param default_version string + --- @return neotest-java.Path + local check_junit_jar = function(filepath, default_version) + local file_exists_fn = check_junit_jar_deps.file_exists or File.exists + local _exists, _ = file_exists_fn(filepath:to_string()) + if not _exists then + -- Try to detect if any supported version exists + local detector = check_junit_jar_deps.version_detector + or JunitVersionDetector({ + exists = function(path) + return File.exists(path:to_string()) + end, + checksum = function(path) + local f = assert(io.open(path:to_string(), "rb")) + local data = f:read("*a") + f:close() + return vim.fn.sha256(data) + end, + scan = require("neotest-java.util.dir_scan"), + stdpath_data = vim.fn.stdpath, + }) + local detected_version, detected_filepath = detector.detect_existing_version() + if detected_version and detected_filepath then + -- Found a supported version, use it + return detected_filepath + end + end + assert( + _exists, + ([[ + Junit Platform Console Standalone jar not found at %s + Please run the following command to download it: NeotestJava setup + Or alternatively, download it from https://repo1.maven.org/maven2/org/junit/platform/junit-platform-console-standalone/%s/junit-platform-console-standalone-%s.jar + ]]):format(filepath, default_version, default_version) + ) + return filepath + end + -- Check for JUnit jar updates (only if user hasn't disabled notifications and not already shown) if not config.disable_update_notifications and not ch.update_notification_shown then local existing_version, _ = version_detector.detect_existing_version() @@ -202,8 +236,11 @@ local function NeotestJavaAdapter(config, deps) return _root_finder.find_root(dir) end, build_spec = function(args) - check_junit_jar(config.junit_jar, config.default_junit_jar_version.version) - return spec_builder_instance.build_spec(args, config) + -- Check if the configured jar exists, if not try to detect any supported version + local actual_jar = check_junit_jar(config.junit_jar, config.default_junit_jar_version.version) + -- Create a config copy with the actual jar to use + local build_config = vim.tbl_extend("force", config, { junit_jar = actual_jar }) + return spec_builder_instance.build_spec(args, build_config) end, }, { __call = function(_, opts, user_deps) diff --git a/tests/unit/init_spec.lua b/tests/unit/init_spec.lua index 0fe06651..b3b52ae1 100644 --- a/tests/unit/init_spec.lua +++ b/tests/unit/init_spec.lua @@ -1,4 +1,6 @@ local default_config = require("neotest-java.default_config") +local Path = require("neotest-java.model.path") +local JunitVersionDetector = require("neotest-java.util.junit_version_detector") local eq = assert.are.same describe("NeotestJava plugin", function() @@ -14,6 +16,74 @@ describe("NeotestJava plugin", function() end end) + it("should use detected version when configured jar does not exist", function() + local Tree = require("neotest.types").Tree + local configured_path = Path("/data/junit-6.0.1.jar") + local detected_path = Path("/data/junit-1.10.1.jar") + local file_exists_calls = {} + local mock_detector = JunitVersionDetector({ + exists = function(_path) + return false + end, + checksum = function(_path) + return "dummy" + end, + scan = function() + return {} + end, + stdpath_data = function() + return "data" + end, + }) + -- Mock detect_existing_version to return a version + mock_detector.detect_existing_version = function() + return { version = "1.10.1", sha256 = "dummy" }, detected_path + end + + local adapter = require("neotest-java")({ + junit_jar = configured_path, + }, { + root_finder = { + find_root = function() + return "/some/root" + end, + }, + check_junit_jar_deps = { + file_exists = function(filepath) + table.insert(file_exists_calls, filepath) + -- Configured jar doesn't exist + return false + end, + version_detector = mock_detector, + }, + }) + + -- Create a minimal tree for testing + local tree = Tree.from_list({ + id = "com.example.Test#testMethod()", + path = "/some/root/src/test/java/com/example/Test.java", + name = "testMethod", + type = "test", + }, function(x) + return x + end) + + -- Try to call build_spec - this should trigger check_junit_jar + -- We expect it to fail because we don't have all dependencies, but it should call check_junit_jar first + pcall(function() + adapter.build_spec({ tree = tree }) + end) + + -- Verify that file_exists was called with the configured path when checking for the jar + assert(#file_exists_calls >= 1, "file_exists should be called at least once") + eq(configured_path:to_string(), file_exists_calls[1]) + + -- The adapter should be initialized successfully + assert(adapter ~= nil) + -- The config should still have the configured path (not modified during init) + eq(configured_path, adapter.config.junit_jar) + end) + it("does not throw when adapter is initialized outside of a java project", function() --- @type neotest-java.Adapter local adapter = require("neotest-java")({}, {