From b9174574946d0d99b2498656146f18b3532bccb0 Mon Sep 17 00:00:00 2001 From: Nil MALHOMME Date: Sun, 9 Aug 2026 19:06:35 +0200 Subject: [PATCH 1/2] refactor: improve logging messages for PluginSystem and add PluginVersionResolver class --- .../ktestify/plugin/PluginRegistry.java | 32 +++--- .../plugin/PluginVersionResolver.java | 105 ++++++++++++++++++ .../plugin/PluginVersionResolverTest.java | 61 ++++++++++ 3 files changed, 180 insertions(+), 18 deletions(-) create mode 100644 src/main/java/io/github/ktestify/plugin/PluginVersionResolver.java create mode 100644 src/test/java/io/github/ktestify/plugin/PluginVersionResolverTest.java diff --git a/src/main/java/io/github/ktestify/plugin/PluginRegistry.java b/src/main/java/io/github/ktestify/plugin/PluginRegistry.java index 3850a4f..270735c 100644 --- a/src/main/java/io/github/ktestify/plugin/PluginRegistry.java +++ b/src/main/java/io/github/ktestify/plugin/PluginRegistry.java @@ -16,17 +16,14 @@ package io.github.ktestify.plugin; import io.github.ktestify.exceptions.PluginException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.File; import java.net.URL; import java.net.URLClassLoader; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.ServiceLoader; +import java.util.*; import java.util.stream.Collectors; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * Discovers, loads, initializes, and holds all active {@link KtestifyPlugin} instances for the current JVM run. @@ -93,7 +90,7 @@ private PluginRegistry(List plugins) { public static PluginRegistry load(PluginContext ctx) { List all = new ArrayList<>(); - LOG.info("╔══ KTestify Plugin System ═════════════════════════════════════"); + LOG.info("Loading plugins..."); // Phase 1 — classpath / fat-jar plugins loadFromClasspath(ctx, all); @@ -103,14 +100,13 @@ public static PluginRegistry load(PluginContext ctx) { loadFromDirectory(pluginsDir, ctx, all); if (all.isEmpty()) { - LOG.info("║ No plugins loaded."); + LOG.info("No plugins loaded."); } else { LOG.info( - "║ {} plugin(s) active: [{}]", + "Plugin system ready: {} plugin(s) active [{}]", all.size(), all.stream().map(p -> p.getId() + "@" + p.getVersion()).collect(Collectors.joining(", "))); } - LOG.info("╚═══════════════════════════════════════════════════════════════"); return new PluginRegistry(all); } @@ -174,7 +170,7 @@ private static void loadFromClasspath(PluginContext ctx, List ta int before = target.size(); for (KtestifyPlugin plugin : loader) { LOG.info( - "║ [classpath] Discovered plugin: {} v{} — author: {} <{}>", + "[classpath] Plugin discovered: {} v{} (author: {} <{}>)", plugin.getId(), plugin.getVersion(), plugin.getAuthorName(), @@ -192,23 +188,23 @@ private static void loadFromClasspath(PluginContext ctx, List ta */ private static void loadFromDirectory(String dirPath, PluginContext ctx, List target) { if (dirPath == null || dirPath.isBlank()) { - LOG.debug("Phase 2 (external): plugins dir not configured — skipping."); + LOG.debug("Phase 2 (external): plugins dir not configured."); return; } File dir = new File(dirPath); if (!dir.exists() || !dir.isDirectory()) { - LOG.debug("Phase 2 (external): directory '{}' does not exist — no external plugins loaded.", dirPath); + LOG.debug("Phase 2 (external): directory '{}' does not exist.", dirPath); return; } File[] jars = dir.listFiles(f -> f.isFile() && f.getName().endsWith(".jar")); if (jars == null || jars.length == 0) { - LOG.debug("Phase 2 (external): no *.jar files found in '{}' — skipping.", dirPath); + LOG.debug("Phase 2 (external): no *.jar files found in '{}'", dirPath); return; } - LOG.info("║ [external] Scanning '{}' — {} JAR(s) found.", dirPath, jars.length); + LOG.info("[external] Scanning '{}' — {} JAR(s) found.", dirPath, jars.length); // Build a single URLClassLoader for all external jars (parent = current context CL) URL[] urls = Arrays.stream(jars) @@ -228,7 +224,7 @@ private static void loadFromDirectory(String dirPath, PluginContext ctx, List (from '{}')", + "[external] Plugin discovered: {} v{} (author: {} <{}>, from '{}')", plugin.getId(), plugin.getVersion(), plugin.getAuthorName(), @@ -245,7 +241,7 @@ private static void loadFromDirectory(String dirPath, PluginContext ctx, ListEach plugin ships a {@code plugin-version.properties} file in its own package directory (e.g. + * {@code /io/github/ktestify/azureblob/plugin-version.properties}). The file contains a single property: + *
+ * plugin.version=${project.version}
+ * 
+ * Maven substitutes {@code ${project.version}} at build time. This approach works in all deployment contexts: + *
    + *
  • Standalone plugin JARs (external plugins loaded via {@code URLClassLoader}) + *
  • The shaded fat JAR (each plugin's file is at a unique package-relative path, so no collision) + *
  • IDE runs (falls back to {@code "dev"} when the file is missing or unfiltered) + *
+ * + *

Plugins should call {@link #resolve(Class, String)} once at class-loading time and store the result in a + * {@code static final} field: + *

+ * private static final String VERSION = PluginVersionResolver.resolve(MyPlugin.class, "dev");
+ * 
+ * + * @since 1.1.1 + */ +public final class PluginVersionResolver { + + private static final Logger LOG = LoggerFactory.getLogger(PluginVersionResolver.class); + + /** Resource file name, looked up relative to the plugin class's package. */ + private static final String RESOURCE_NAME = "plugin-version.properties"; + + /** Property key inside the {@code plugin-version.properties} file. */ + private static final String PROPERTY_KEY = "plugin.version"; + + private PluginVersionResolver() {} + + /** + * Resolves the plugin version from the Maven-filtered {@code plugin-version.properties} file located in the same + * package as the given plugin class. + * + *

If the file is missing, empty, or contains an unfiltered {@code ${project.version}} placeholder (e.g. when + * running from an IDE without Maven resource filtering), the fallback value is returned. + * + * @param pluginClass the plugin's main class, used to locate the resource in the correct package + * @param fallback the version to return if the resource cannot be resolved + * @return the resolved version string, or the fallback + */ + public static String resolve(Class pluginClass, String fallback) { + try (InputStream is = pluginClass.getResourceAsStream(RESOURCE_NAME)) { + if (is == null) { + LOG.debug( + "No '{}' found next to {}, using fallback version '{}'.", + RESOURCE_NAME, + pluginClass.getName(), + fallback); + return fallback; + } + Properties props = new Properties(); + props.load(is); + String version = props.getProperty(PROPERTY_KEY); + if (version == null || version.isBlank() || version.startsWith("${")) { + LOG.debug( + "Property '{}' in '{}' is unfiltered or blank for {}, using fallback version '{}'.", + PROPERTY_KEY, + RESOURCE_NAME, + pluginClass.getName(), + fallback); + return fallback; + } + return version; + } catch (IOException e) { + LOG.warn( + "Failed to read '{}' for {}, using fallback version '{}': {}", + RESOURCE_NAME, + pluginClass.getName(), + fallback, + e.getMessage()); + return fallback; + } + } +} + diff --git a/src/test/java/io/github/ktestify/plugin/PluginVersionResolverTest.java b/src/test/java/io/github/ktestify/plugin/PluginVersionResolverTest.java new file mode 100644 index 0000000..9792550 --- /dev/null +++ b/src/test/java/io/github/ktestify/plugin/PluginVersionResolverTest.java @@ -0,0 +1,61 @@ +/* + * Copyright 2026 Nil MALHOMME (malhomme.nil+oss@icloud.com) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.github.ktestify.plugin; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Unit tests for {@link PluginVersionResolver}. + * + *

Verifies fallback behaviour when the {@code plugin-version.properties} resource is missing or unfiltered, which is + * the expected state in IDE runs and unit test contexts. + */ +@DisplayName("PluginVersionResolver") +class PluginVersionResolverTest { + + @Test + @DisplayName("resolve() returns fallback when no plugin-version.properties is found next to the class") + void returnsFallbackWhenResourceMissing() { + // This test class has no plugin-version.properties next to it + String version = PluginVersionResolver.resolve(PluginVersionResolverTest.class, "dev"); + assertEquals("dev", version); + } + + @Test + @DisplayName("resolve() returns fallback when fallback is a real version string") + void returnsFallbackVersionString() { + String version = PluginVersionResolver.resolve(PluginVersionResolverTest.class, "1.2.3"); + assertEquals("1.2.3", version); + } + + @Test + @DisplayName("resolve() returns fallback when fallback is blank") + void returnsBlankFallback() { + String version = PluginVersionResolver.resolve(PluginVersionResolverTest.class, ""); + assertEquals("", version); + } + + @Test + @DisplayName("resolve() on PluginVersionResolver itself returns fallback (no resource next to it)") + void resolveOnItselfReturnsFallback() { + String version = PluginVersionResolver.resolve(PluginVersionResolver.class, "test"); + assertEquals("test", version); + } +} + From 804efc4b25f8a904b4e881701b336ecb26f58292 Mon Sep 17 00:00:00 2001 From: Nil MALHOMME Date: Sun, 9 Aug 2026 19:36:15 +0200 Subject: [PATCH 2/2] refactor: Refactored plugin init logs --- .../java/io/github/ktestify/plugin/PluginRegistry.java | 5 ++--- .../github/ktestify/plugin/PluginVersionResolver.java | 10 ++++++---- .../ktestify/plugin/PluginVersionResolverTest.java | 5 ++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/main/java/io/github/ktestify/plugin/PluginRegistry.java b/src/main/java/io/github/ktestify/plugin/PluginRegistry.java index 270735c..fedd2ca 100644 --- a/src/main/java/io/github/ktestify/plugin/PluginRegistry.java +++ b/src/main/java/io/github/ktestify/plugin/PluginRegistry.java @@ -16,14 +16,13 @@ package io.github.ktestify.plugin; import io.github.ktestify.exceptions.PluginException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.File; import java.net.URL; import java.net.URLClassLoader; import java.util.*; import java.util.stream.Collectors; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Discovers, loads, initializes, and holds all active {@link KtestifyPlugin} instances for the current JVM run. diff --git a/src/main/java/io/github/ktestify/plugin/PluginVersionResolver.java b/src/main/java/io/github/ktestify/plugin/PluginVersionResolver.java index 7f9fa2e..0d7d616 100644 --- a/src/main/java/io/github/ktestify/plugin/PluginVersionResolver.java +++ b/src/main/java/io/github/ktestify/plugin/PluginVersionResolver.java @@ -15,22 +15,24 @@ */ package io.github.ktestify.plugin; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.IOException; import java.io.InputStream; import java.util.Properties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Resolves a plugin's build version from a Maven-filtered {@code plugin-version.properties} file on the classpath. * *

Each plugin ships a {@code plugin-version.properties} file in its own package directory (e.g. * {@code /io/github/ktestify/azureblob/plugin-version.properties}). The file contains a single property: + * *

  * plugin.version=${project.version}
  * 
+ * * Maven substitutes {@code ${project.version}} at build time. This approach works in all deployment contexts: + * *
    *
  • Standalone plugin JARs (external plugins loaded via {@code URLClassLoader}) *
  • The shaded fat JAR (each plugin's file is at a unique package-relative path, so no collision) @@ -39,6 +41,7 @@ * *

    Plugins should call {@link #resolve(Class, String)} once at class-loading time and store the result in a * {@code static final} field: + * *

      * private static final String VERSION = PluginVersionResolver.resolve(MyPlugin.class, "dev");
      * 
    @@ -102,4 +105,3 @@ public static String resolve(Class pluginClass, String fallback) { } } } - diff --git a/src/test/java/io/github/ktestify/plugin/PluginVersionResolverTest.java b/src/test/java/io/github/ktestify/plugin/PluginVersionResolverTest.java index 9792550..185a022 100644 --- a/src/test/java/io/github/ktestify/plugin/PluginVersionResolverTest.java +++ b/src/test/java/io/github/ktestify/plugin/PluginVersionResolverTest.java @@ -15,11 +15,11 @@ */ package io.github.ktestify.plugin; +import static org.junit.jupiter.api.Assertions.assertEquals; + import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.assertEquals; - /** * Unit tests for {@link PluginVersionResolver}. * @@ -58,4 +58,3 @@ void resolveOnItselfReturnsFallback() { assertEquals("test", version); } } -