diff --git a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/EmbeddedResourceLoader.java b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/EmbeddedResourceLoader.java index 3813068a3d..1ae4227236 100644 --- a/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/EmbeddedResourceLoader.java +++ b/java/semantickernel-api/src/main/java/com/microsoft/semantickernel/implementation/EmbeddedResourceLoader.java @@ -51,12 +51,7 @@ public class EmbeddedResourceLoader { type -> { switch (type) { case CLASSPATH: - try (InputStream inputStream = clazz.getResourceAsStream(fileName)) { - return readInputStream(fileName, inputStream); - } catch (Exception e) { - // IGNORE - } - break; + return getResourceAsStream(fileName, clazz); case CLASSPATH_ROOT: try (InputStream inputStream = Thread.currentThread() .getContextClassLoader() @@ -67,16 +62,7 @@ public class EmbeddedResourceLoader { } break; case FILESYSTEM: - File file = new File(fileName); - if (file.exists()) { - try ( - InputStream inputStream = Files.newInputStream(file.toPath())) { - return readInputStream(fileName, inputStream); - } catch (IOException e) { - // IGNORE - } - } - break; + return readFileFromFileSystem(fileName); default: } return null; @@ -91,16 +77,40 @@ public class EmbeddedResourceLoader { throw new FileNotFoundException("Could not find file " + fileName); } + static String getResourceAsStream(String fileName, Class clazz) { + try (InputStream inputStream = clazz.getResourceAsStream(fileName)) { + return readInputStream(fileName, inputStream); + } catch (Exception e) { + // IGNORE + } + return null; + } + + static String readFileFromFileSystem(String fileName) { + File file = new File(fileName); + if (file.exists()) { + try (InputStream inputStream = Files.newInputStream(file.toPath())) { + return readInputStream(fileName, inputStream); + } catch (IOException e) { + // IGNORE + } + } + return null; + } + private static String readInputStream(String fileName, InputStream inputStream) throws FileNotFoundException { if (inputStream == null) { throw new FileNotFoundException("File not found: " + fileName); } - return new BufferedReader( - new InputStreamReader(inputStream, java.nio.charset.StandardCharsets.UTF_8)) - .lines() - .collect(Collectors.joining("\n")); + try (BufferedReader bf = new BufferedReader( + new InputStreamReader(inputStream, java.nio.charset.StandardCharsets.UTF_8))) { + return bf.lines().collect(Collectors.joining("\n")); + } catch (IOException e) { + // IGNORE + } + return null; } /** diff --git a/java/semantickernel-api/src/test/java/com/microsoft/semantickernel/implementation/EmbeddedResourceLoaderTest.java b/java/semantickernel-api/src/test/java/com/microsoft/semantickernel/implementation/EmbeddedResourceLoaderTest.java new file mode 100644 index 0000000000..6ec14af591 --- /dev/null +++ b/java/semantickernel-api/src/test/java/com/microsoft/semantickernel/implementation/EmbeddedResourceLoaderTest.java @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft. All rights reserved. +package com.microsoft.semantickernel.implementation; + +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.CALLS_REAL_METHODS; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; + +import java.io.ByteArrayInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +public class EmbeddedResourceLoaderTest { + + @Test + public void testReadFile_Classpath() { + assertThrows(FileNotFoundException.class, () -> { + EmbeddedResourceLoader.readFile("nonexistent.txt", this.getClass()); + }); + } + + @Test + public void testReadFile_ClasspathRoot() { + assertThrows(FileNotFoundException.class, () -> { + EmbeddedResourceLoader.readFile("nonexistent.txt", this.getClass(), + EmbeddedResourceLoader.ResourceLocation.CLASSPATH_ROOT); + }); + } + + @Test + public void testReadFile_FileSystem() { + assertThrows(FileNotFoundException.class, () -> { + EmbeddedResourceLoader.readFile("nonexistent.txt", this.getClass(), + EmbeddedResourceLoader.ResourceLocation.FILESYSTEM); + }); + } + + @Test + public void testReadFile_Classpath_ExistingFile() throws FileNotFoundException { + try (MockedStatic mocked = Mockito.mockStatic( + EmbeddedResourceLoader.class, + withSettings().defaultAnswer(CALLS_REAL_METHODS))) { + + mocked + .when(() -> EmbeddedResourceLoader.getResourceAsStream(any(String.class), + any(Class.class))) + .thenReturn("file content"); + + String result = EmbeddedResourceLoader.readFile("existent.txt", + EmbeddedResourceLoaderTest.class); + assertEquals("file content", result); + } + } + + @Test + public void testReadFile_ClasspathRoot_ExistingFile() throws FileNotFoundException { + ClassLoader mockClassLoader = Mockito.mock(ClassLoader.class); + + InputStream inputStream = new ByteArrayInputStream( + "file content".getBytes(StandardCharsets.UTF_8)); + when(mockClassLoader.getResourceAsStream(any(String.class))).thenReturn(inputStream); + + Thread.currentThread().setContextClassLoader(mockClassLoader); + + String result = EmbeddedResourceLoader.readFile("existent.txt", + EmbeddedResourceLoaderTest.class, + EmbeddedResourceLoader.ResourceLocation.CLASSPATH_ROOT); + assertEquals("file content", result); + } + + @Test + public void testReadFile_FileSystem_ExistingFile() throws IOException { + try (MockedStatic mocked = Mockito.mockStatic( + EmbeddedResourceLoader.class, + withSettings().defaultAnswer(CALLS_REAL_METHODS))) { + + mocked.when(() -> EmbeddedResourceLoader.readFileFromFileSystem(any(String.class))) + .thenReturn("file content"); + + String result = EmbeddedResourceLoader.readFile("existent.txt", + EmbeddedResourceLoaderTest.class, + EmbeddedResourceLoader.ResourceLocation.FILESYSTEM); + assertEquals("file content", result); + } + } +} \ No newline at end of file diff --git a/java/semantickernel-api/src/test/java/com/microsoft/semantickernel/templateengine/handlebars/HandlebarsPromptTemplateTest.java b/java/semantickernel-api/src/test/java/com/microsoft/semantickernel/templateengine/handlebars/HandlebarsPromptTemplateTest.java index c978d64fc4..4acf33f1f9 100644 --- a/java/semantickernel-api/src/test/java/com/microsoft/semantickernel/templateengine/handlebars/HandlebarsPromptTemplateTest.java +++ b/java/semantickernel-api/src/test/java/com/microsoft/semantickernel/templateengine/handlebars/HandlebarsPromptTemplateTest.java @@ -1,27 +1,4 @@ -/* - * The MIT License - * - * Copyright 2024 davidgrieve. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - +// Copyright (c) Microsoft. All rights reserved. package com.microsoft.semantickernel.templateengine.handlebars; import java.util.Arrays; @@ -54,24 +31,23 @@ public class HandlebarsPromptTemplateTest { public HandlebarsPromptTemplateTest() { } - public static void main(String[] args) { new HandlebarsPromptTemplateTest().testRenderAsync(); } + public static void main(String[] args) { + new HandlebarsPromptTemplateTest().testRenderAsync(); + } public static class StringFunctions { @DefineKernelFunction(name = "upper", description = "Converts a string to upper case.") public String upper( - @KernelFunctionParameter(name = "input", required = true, description = "The string to convert to upper case", type = String.class) - String input) { - return input.toUpperCase(); + @KernelFunctionParameter(name = "input", required = true, description = "The string to convert to upper case", type = String.class) String input) { + return input.toUpperCase(); } @DefineKernelFunction(name = "concat", description = "Concatenate the second string to the first string.") public String concat( - @KernelFunctionParameter(name = "input", required = true, description = "The string to which the second string is concatenated.", type = String.class) - String first, - @KernelFunctionParameter(name = "suffix", required = true, description = "The string which is concatenated to the first string.", type = String.class) - String suffix) { - return first.concat(suffix); + @KernelFunctionParameter(name = "input", required = true, description = "The string to which the second string is concatenated.", type = String.class) String first, + @KernelFunctionParameter(name = "suffix", required = true, description = "The string which is concatenated to the first string.", type = String.class) String suffix) { + return first.concat(suffix); } } @@ -88,39 +64,33 @@ public class HandlebarsPromptTemplateTest { new ChatHistory( Arrays.asList( new ChatMessageContent(AuthorRole.SYSTEM, "a"), - new ChatMessageContent(AuthorRole.USER, "b") - ) - ), + new ChatMessageContent(AuthorRole.USER, "b"))), new ChatHistory( Arrays.asList( new ChatMessageContent(AuthorRole.SYSTEM, "c"), - new ChatMessageContent(AuthorRole.USER, "d") - ) - ) - ); + new ChatMessageContent(AuthorRole.USER, "d")))); KernelPlugin kernelPlugin = KernelPluginFactory.createFromObject( new StringFunctions(), - "string" - ); + "string"); Kernel kernel = Kernel.builder() .withPlugin(kernelPlugin) .build(); PromptTemplateConfig promptTemplate = PromptTemplateConfig.builder() - .withTemplate( + .withTemplate( "{{choices.[0]}}\n" + - "{{choices}}\n" + - "{{#each history}}\n" + - " {{#each this}}\n" + - " {{string-upper content}}\n" + - " {{/each}}\n" + - "{{/each}}\n" + - "Hello World") - // "{{string-concat input suffix}}") TODO - this is not working + "{{choices}}\n" + + "{{#each history}}\n" + + " {{#each this}}\n" + + " {{string-upper content}}\n" + + " {{/each}}\n" + + "{{/each}}\n" + + "Hello World") + // "{{string-concat input suffix}}") TODO - this is not working .withTemplateFormat("handlebars") - .build(); + .build(); HandlebarsPromptTemplate instance = new HandlebarsPromptTemplate(promptTemplate); @@ -130,15 +100,15 @@ public class HandlebarsPromptTemplateTest { .withVariable("choices", choices) .withVariable("history", history) .withVariable("kernelPlugins", Arrays.asList(kernelPlugin)) - .build(); - + .build(); + // Return from renderAsync is normalized to remove empty lines and leading/trailing whitespace String expResult = "CHOICE-A [CHOICE-A, CHOICE-B] A B C D Hello World"; String result = instance.renderAsync(kernel, arguments, null).block(); assertNotNull(result); - String normalizedResult = + String normalizedResult = // split result into lines Arrays.stream(result.split("\\r?\\n|\\r")) // remove leading and trailing whitespace