diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml new file mode 100644 index 0000000..b08eae1 --- /dev/null +++ b/.github/workflows/unit-test.yml @@ -0,0 +1,56 @@ +name: Run Unit Tests + +on: + pull_request: + branches: [ default-app ] + workflow_dispatch: + inputs: + snapshot: + description: 'CI version snapshot' + required: true + default: '5.0.0-alpha-SNAPSHOT' + type: choice + options: + - '5.0.0-alpha-SNAPSHOT' + clean_install: + description: 'Run clean install with -U flag if true, else false' + required: true + default: "false" + +jobs: + test: + name: Run Test Suite + runs-on: ubuntu-latest + container: maven:3.9.9-amazoncorretto-21-debian + env: + SNAPSHOT: ${{ inputs.snapshot || '5.0.0-alpha-SNAPSHOT' }} + CLEAN_INSTALL: ${{ inputs.clean_install || 'false' }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Cache Maven packages + uses: actions/cache@v4 + with: + path: /root/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Maven Install + run: | + if [ "${CLEAN_INSTALL}" = "true" ] || [ -z "${CLEAN_INSTALL}" ]; then + echo "clean install: ${CLEAN_INSTALL}" + echo "snapshot: ${SNAPSHOT}" + echo "Running mvn clean install with -U flag" + mvn clean install -U -DskipTests=true -Dci.version="${SNAPSHOT}" | grep -v "^Progress (1):" + else + echo "Running mvn install without -U flag" + mvn install -DskipTests=true -Dci.version="${SNAPSHOT}" | grep -v "^Progress (1):" + fi + + - name: Run Unit Tests + run: | + mvn test -Dci.version="${SNAPSHOT}" | grep -v "^Progress (1):" + \ No newline at end of file diff --git a/.gitignore b/.gitignore index 904a704..768935e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ client/node_modules/ portals/ classes/ target/ +test-classes/ # Local configuration files *.local diff --git a/README.md b/README.md index 9771e8e..fde095b 100644 --- a/README.md +++ b/README.md @@ -115,6 +115,7 @@ This repository includes several tools to help maintain code quality: - See `client/README.md` for front-end development instructions. - See `java/README.md` for back-end/reactor development. +- See `test/README.md` for comprehensive testing guide and workflow. --- diff --git a/pom.xml b/pom.xml index d9d575c..65c52c2 100644 --- a/pom.xml +++ b/pom.xml @@ -9,7 +9,9 @@ java/src + test classes + test-classes java @@ -61,6 +63,26 @@ false + + public + Sonatype's Maven repository + https://oss.sonatype.org/content/groups/public + + true + + + false + + + + sonatype-snapshots + Sonatype Snapshots + https://central.sonatype.com/repository/maven-snapshots + + true + always + + 3rdPartyJARs Maven repository @@ -80,6 +102,38 @@ ${ci.version} provided + + + + org.junit.jupiter + junit-jupiter + 6.0.0 + test + + + + org.junit.platform + junit-platform-suite + 6.0.0 + test + + + + org.mockito + mockito-core + 5.18.0 + test + + + net.bytebuddy + byte-buddy + + + org.objenesis + objenesis + + + @@ -89,7 +143,6 @@ 3.6.3 21 - 21 UTF-8 private true diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..63dabaa --- /dev/null +++ b/test/README.md @@ -0,0 +1,371 @@ +# Reactor Test Suite + +Comprehensive test suite for SEMOSS Template project reactors with organized test structure, helper utilities, and extensive test coverage. + +## Test Structure + +``` +test/ +├── reactors/ +│ ├── BaseReactorTest.java # Base test class with common mocking utilities +│ ├── ReactorTestSuite.java # Test suite to run all tests together +│ └── example/ +│ ├── HelloReactorTest.java # Tests for HelloUserReactor +│ ├── CallPythonReactorTest.java # Tests for CallPythonReactor +│ └── OpenMCPAppReactorTest.java # Tests for OpenMCPAppReactor +``` + +## Running Tests + +### Run All Tests in the Suite + +```bash +mvn test -Dtest=ReactorTestSuite +``` + +### Run Individual Test Classes + +```bash +# Run HelloUserReactor tests +mvn test -Dtest=HelloReactorTest + +# Run CallPythonReactor tests +mvn test -Dtest=CallPythonReactorTest + +# Run OpenMCPAppReactor tests +mvn test -Dtest=OpenMCPAppReactorTest +``` + +### Run All Tests + +```bash +mvn test +``` + +### Run Specific Test Method + +```bash +mvn test -Dtest=HelloReactorTest#testHelloUserReactor_CustomName +``` + +## Testing Workflow + +### Development Cycle + +Testing should be an integral part of your reactor development process: + +1. **Design Phase** - Plan your reactor's functionality and identify test scenarios +2. **Implementation** - Write your reactor code in `java/src/reactors/` +3. **Test Creation** - Create corresponding test class in `test/reactors/` +4. **Validation** - Run tests to verify behavior +5. **Iteration** - Fix issues and re-run tests until all pass +6. **Integration** - Add test to suite and commit + +### Recommended Testing Workflow + +#### Option 1: Test-Driven Development (TDD) +Write tests before implementing the reactor: + +```bash +# 1. Create test class first +# test/reactors/example/YourReactorTest.java + +# 2. Run tests (they will fail) +mvn test -Dtest=YourReactorTest + +# 3. Implement reactor to make tests pass +# java/src/reactors/examples/YourReactor.java + +# 4. Run tests again +mvn test -Dtest=YourReactorTest + +# 5. Refactor and repeat until all tests pass +``` + +#### Option 2: Traditional Development +Write reactor first, then add tests: + +```bash +# 1. Implement reactor +# java/src/reactors/examples/YourReactor.java + +# 2. Create comprehensive tests +# test/reactors/example/YourReactorTest.java + +# 3. Run tests to verify +mvn test -Dtest=YourReactorTest + +# 4. Fix any issues discovered +``` + +### Pre-Commit Testing + +Always run tests before committing changes: + +```bash +# Run all tests +mvn test + +# Or run just the tests for modified reactors +mvn test -Dtest=YourModifiedReactorTest + +# Stage and commit only after tests pass +git add . +git commit -m "feat: Add YourReactor with comprehensive tests" +``` + +### Continuous Testing During Development + +For rapid feedback during active development: + +```bash +# Terminal 1: Keep this running +mvn test -Dtest=YourReactorTest + +# Terminal 2: Edit your code +# Make changes to reactor or test + +# Return to Terminal 1 and re-run after each change +``` + +### Integration with SEMOSS Development + +When working with the SEMOSS UI: + +1. **Before "Recompile reactors"** in SEMOSS UI: + ```bash + mvn test # Ensure tests pass + ``` + +2. **After compiling** in SEMOSS UI: + - Test reactor in the application + - If issues found, update tests to cover the bug + - Fix reactor code + - Re-run tests + +3. **Before "Publish files"**: + ```bash + mvn test # Final verification + ``` + +### Multi-Reactor Development + +When working on multiple reactors: + +```bash +# Run tests for specific package +mvn test -Dtest=reactors.example.*Test + +# Or run the full suite +mvn test -Dtest=ReactorTestSuite +``` + +### Debugging Failed Tests + +1. **Read the error message** - JUnit provides detailed failure information +2. **Check mock setup** - Verify mocks are configured correctly +3. **Add debug logging** - Use `System.out.println()` in tests temporarily +4. **Run in debug mode** - Use your IDE's debugger to step through +5. **Isolate the issue** - Run single test method to focus + +```bash +# Run single test method with verbose output +mvn test -Dtest=YourReactorTest#testSpecificScenario -X +``` + +### Workflow Best Practices + +✅ **Do:** +- Run tests frequently during development +- Write tests for bug fixes before fixing the bug +- Keep tests fast and focused +- Run full test suite before pushing to remote +- Update test documentation when adding new tests + +❌ **Don't:** +- Skip writing tests for "simple" reactors +- Commit code with failing tests +- Ignore test failures in CI/CD +- Write overly complex tests that are hard to maintain +- Test implementation details instead of behavior + +## Test Coverage + +### HelloUserReactor Tests +- ✅ Default user greeting (no parameters) +- ✅ Custom name parameter +- ✅ Empty string name parameter + +### CallPythonReactor Tests +- ✅ Fibonacci calculation for input 0 +- ✅ Fibonacci calculation for input 1 +- ✅ Fibonacci calculation for input 5 +- ✅ Fibonacci calculation for input 10 +- ✅ Fibonacci calculation for input 20 (large number) +- ✅ Argument list verification + +### OpenMCPAppReactor Tests +- ✅ Returns placeholder message +- ✅ Exact message verification +- ✅ No parameters required +- ✅ Reactor description verification +- ✅ Multiple executions consistency + +## BaseReactorTest Utilities + +The `BaseReactorTest` class provides common mocking utilities for all reactor tests: + +### Provided Mocks +- `@Mock Insight insight` - Mock insight for execution context +- `@Mock User user` - Mock user for authentication +- `@Mock NounStore nounStore` - Mock parameter storage +- `@Mock PyTranslator pyTranslator` - Mock Python integration +- `MockedStatic assetUtilsMock` - Mock asset utilities +- `Path tempDir` - Temporary directory for test files + +### Helper Methods + +#### Setting Reactor Parameters +```java +// Set string parameter +setReactorParameter(reactor, ReactorKeysEnum.NAME.getKey(), "Alice"); + +// Set numeric parameter +setReactorParameter(reactor, ReactorKeysEnum.NUMERIC_VALUE.getKey(), 42); +``` + +#### Python Integration Setup +```java +// Mock Python module loading and function execution +setupPyTranslatorMocks("moduleName", "functionName", returnValue); +``` + +#### Creating Test Files +```java +// Create a Python file in the temp directory +createPythonFile("script.py", pythonCode); +``` + +#### Custom Project Properties +```java +@Override +protected void configureProjectProperties(Properties props) { + props.put("custom.property", "value"); +} +``` + +## Writing New Tests + +### Basic Test Structure + +```java +package reactors.example; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import reactors.BaseReactorTest; +import reactors.examples.YourReactor; +import static org.junit.jupiter.api.Assertions.*; + +@DisplayName("YourReactor Tests") +public class YourReactorTest extends BaseReactorTest { + + private YourReactor reactor; + + @BeforeEach + void setup() { + reactor = new YourReactor(); + reactor.setInsight(insight); + reactor.setNounStore(nounStore); + } + + @Test + @DisplayName("Description of what this test does") + public void testYourReactor_Scenario() { + // Arrange: Set up parameters + setReactorParameter(reactor, "paramKey", "paramValue"); + + // Act: Execute reactor + NounMetadata result = reactor.execute(); + + // Assert: Verify results + assertNotNull(result); + assertEquals(PixelDataType.CONST_STRING, result.getNounType()); + } +} +``` + +### Add to Test Suite + +Update `ReactorTestSuite.java` to include your new test class: + +```java +@SelectClasses({ + HelloReactorTest.class, + CallPythonReactorTest.class, + OpenMCPAppReactorTest.class, + YourNewReactorTest.class // Add here +}) +``` + +## Test Dependencies + +All required dependencies are already configured in `pom.xml`: + +- **JUnit Jupiter 6.0.0** - Testing framework +- **JUnit Platform Suite 6.0.0** - Test suite support +- **Mockito 5.18.0** - Mocking framework + +## Best Practices + +1. **Extend BaseReactorTest** - Always extend `BaseReactorTest` for new reactor tests +2. **Use @DisplayName** - Add descriptive display names to tests and test classes +3. **Arrange-Act-Assert** - Follow AAA pattern in test methods +4. **Test Multiple Scenarios** - Test happy path, edge cases, and error conditions +5. **Use Helper Methods** - Leverage `BaseReactorTest` helper methods for cleaner tests +6. **Mock External Dependencies** - Use provided mocks for PyTranslator, AssetUtility, etc. +7. **Verify Interactions** - Use Mockito's `verify()` to ensure proper method calls + +## Continuous Integration + +These tests are designed to run in CI/CD pipelines. Ensure your CI configuration includes: + +```yaml +# Example for GitHub Actions +- name: Run Tests + run: mvn test +``` + +## Troubleshooting + +### Tests Failing Due to Missing Dependencies +```bash +mvn clean install +``` + +### Cannot Find Test Classes +Ensure the test source directory is correctly configured in `pom.xml`: +```xml +test +``` + +### Mock Setup Issues +Verify that `MockitoAnnotations.openMocks(this)` is called in `BaseReactorTest.baseSetup()` + +### Python Integration Tests Failing +Ensure `setupPyTranslatorMocks()` is called with correct module and function names + +## Additional Resources + +- [JUnit 5 User Guide](https://junit.org/junit5/docs/current/user-guide/) +- [Mockito Documentation](https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html) +- [SEMOSS Documentation](https://semoss.org/docs) + +## Contributing + +When adding new reactors, please: +1. Create corresponding test classes extending `BaseReactorTest` +2. Add comprehensive test coverage (minimum 3-5 test cases) +3. Update `ReactorTestSuite.java` to include new tests +4. Update this README with test coverage details diff --git a/test/reactors/BaseReactorTest.java b/test/reactors/BaseReactorTest.java new file mode 100644 index 0000000..3038ed6 --- /dev/null +++ b/test/reactors/BaseReactorTest.java @@ -0,0 +1,273 @@ +package reactors; + +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.Properties; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.io.TempDir; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import prerna.auth.AccessToken; +import prerna.auth.AuthProvider; +import prerna.auth.User; +import prerna.ds.py.PyTranslator; +import prerna.om.Insight; +import prerna.sablecc2.om.NounStore; +import prerna.util.AssetUtility; + +/** + * Base test class providing common mocking utilities and setup for reactor + * tests. + * All reactor test classes should extend this class to leverage shared test + * infrastructure. + * + *

+ * This class handles: + *

    + *
  • Mock setup and teardown for common SEMOSS components
  • + *
  • Temporary directory management for project assets
  • + *
  • User authentication mocking
  • + *
  • Project property file creation
  • + *
  • PyTranslator mocking for Python-based reactors
  • + *
+ */ +public abstract class BaseReactorTest { + + /** Mock insight providing context for reactor execution */ + @Mock + protected Insight insight; + + /** Mock user for authentication context */ + @Mock + protected User user; + + /** Mock noun store for reactor parameter management */ + @Mock + protected NounStore nounStore; + + /** Mock PyTranslator for Python integration testing */ + @Mock + protected PyTranslator pyTranslator; + + /** Static mock for AssetUtility to control project asset paths */ + protected MockedStatic assetUtilsMock; + + /** AutoCloseable for managing Mockito annotations lifecycle */ + private AutoCloseable mocks; + + /** Temporary directory for test execution */ + protected Path tempDir; + + /** Fake project ID for testing */ + protected static final String TEST_PROJECT_ID = "test-project-id"; + + /** Default test user name */ + protected static final String TEST_USER_NAME = "TestUser"; + + /** + * Sets up the test environment before each test execution. + * This method initializes mocks, creates temporary directories, and sets up + * common mock behaviors for insight, user, and asset utilities. + * + * @param p Temporary directory provided by JUnit + * @throws IOException if file operations fail during setup + */ + @BeforeEach + void baseSetup(@TempDir Path p) throws IOException { + tempDir = p; + mocks = MockitoAnnotations.openMocks(this); + + setupInsightMocks(); + setupUserMocks(); + setupAssetUtilityMocks(); + setupProjectProperties(); + } + + /** + * Sets up mock behaviors for the Insight object. + * Configures project ID retrieval and user context. + */ + protected void setupInsightMocks() { + when(insight.getUser()).thenReturn(user); + when(insight.getContextProjectId()).thenReturn(TEST_PROJECT_ID); + when(insight.getProjectId()).thenReturn(TEST_PROJECT_ID); + when(insight.getPyTranslator()).thenReturn(pyTranslator); + } + + /** + * Sets up mock behaviors for the User object. + * Creates a mock access token with test user credentials. + */ + protected void setupUserMocks() { + AccessToken token = new AccessToken(); + token.setName(TEST_USER_NAME); + token.setProvider(AuthProvider.NATIVE); + when(user.getPrimaryLoginToken()).thenReturn(token); + } + + /** + * Sets up static mocking for AssetUtility. + * Configures the asset utility to return the temporary directory as the project + * assets folder. + */ + protected void setupAssetUtilityMocks() { + assetUtilsMock = Mockito.mockStatic(AssetUtility.class); + assetUtilsMock.when(() -> AssetUtility.getProjectAssetsFolder(TEST_PROJECT_ID)) + .thenReturn(tempDir.toAbsolutePath().toString()); + } + + /** + * Creates a project.properties file in the temporary directory. + * Subclasses can override {@link #configureProjectProperties(Properties)} to + * add custom properties. + * + * @throws IOException if file creation or writing fails + */ + protected void setupProjectProperties() throws IOException { + Properties props = new Properties(); + configureProjectProperties(props); + + Path javaDir = tempDir.resolve("java"); + Files.createDirectories(javaDir); + Path projectPropertiesFile = javaDir.resolve("project.properties"); + + try (OutputStream os = Files.newOutputStream(projectPropertiesFile, + StandardOpenOption.WRITE, StandardOpenOption.CREATE)) { + props.store(os, "Test project properties"); + } + } + + /** + * Hook method for subclasses to configure custom project properties. + * Override this method to add specific properties needed for your reactor + * tests. + * + * @param props Properties object to configure + */ + protected void configureProjectProperties(Properties props) { + // Default implementation - subclasses can override to add custom properties + } + + /** + * Sets up mock behaviors for PyTranslator. + * Configures default responses for Python module loading and function + * execution. + * + * @param moduleName The name of the Python module to mock + * @param functionName The name of the Python function to mock + * @param returnValue The value to return when the function is called + */ + protected void setupPyTranslatorMocks(String moduleName, String functionName, Object returnValue) { + when(pyTranslator.loadPythonModuleFromFile( + Mockito.eq(insight), + Mockito.anyString(), + Mockito.eq(TEST_PROJECT_ID))) + .thenReturn(moduleName); + + when(pyTranslator.runFunctionFromLoadedModule( + Mockito.eq(insight), + Mockito.eq(moduleName), + Mockito.eq(functionName), + Mockito.anyList())) + .thenReturn(returnValue); + } + + /** + * Creates a Python source file in the temporary directory for testing. + * Useful for reactors that need to load Python files. + * + * @param fileName The name of the Python file to create + * @param content The content of the Python file + * @throws IOException if file creation fails + */ + protected void createPythonFile(String fileName, String content) throws IOException { + Path pyDir = tempDir.resolve("py"); + Files.createDirectories(pyDir); + Path pythonFile = pyDir.resolve(fileName); + Files.writeString(pythonFile, content); + } + + /** + * Helper method to set a parameter value on a reactor. + * This directly sets the value in the reactor's keyValue map via reflection. + * + * @param reactor The reactor to set the parameter on + * @param key The parameter key + * @param value The parameter value + */ + protected void setReactorParameter(reactors.AbstractProjectReactor reactor, String key, String value) { + try { + // Access the protected keyValue map field via reflection + java.lang.reflect.Field keyValueField = findField(reactor.getClass(), "keyValue"); + keyValueField.setAccessible(true); + + @SuppressWarnings("unchecked") + java.util.Map keyValueMap = (java.util.Map) keyValueField.get(reactor); + + // Initialize the map if it's null + if (keyValueMap == null) { + keyValueMap = new java.util.HashMap<>(); + keyValueField.set(reactor, keyValueMap); + } + + // Add the parameter to the map + keyValueMap.put(key, value); + + } catch (Exception e) { + throw new RuntimeException("Failed to set reactor parameter: " + key, e); + } + } + + /** + * Helper method to find a field in the class hierarchy. + */ + private java.lang.reflect.Field findField(Class clazz, String fieldName) throws NoSuchFieldException { + Class current = clazz; + while (current != null) { + try { + return current.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + current = current.getSuperclass(); + } + } + throw new NoSuchFieldException(fieldName); + } + + /** + * Helper method to set a numeric parameter on a reactor. + * Converts the number to a string automatically. + * + * @param reactor The reactor to set the parameter on + * @param key The parameter key + * @param value The numeric value + */ + protected void setReactorParameter(reactors.AbstractProjectReactor reactor, String key, int value) { + setReactorParameter(reactor, key, String.valueOf(value)); + } + + /** + * Cleans up test resources after each test execution. + * Closes static mocks and Mockito annotations. + * + * @throws Exception if cleanup fails + */ + @AfterEach + void baseTearDown() throws Exception { + if (assetUtilsMock != null) { + assetUtilsMock.close(); + } + if (mocks != null) { + mocks.close(); + } + } +} diff --git a/test/reactors/ReactorTestSuite.java b/test/reactors/ReactorTestSuite.java new file mode 100644 index 0000000..9f12752 --- /dev/null +++ b/test/reactors/ReactorTestSuite.java @@ -0,0 +1,41 @@ +package reactors; + +import org.junit.platform.suite.api.SelectClasses; +import org.junit.platform.suite.api.Suite; +import org.junit.platform.suite.api.SuiteDisplayName; + +import reactors.example.CallPythonReactorTest; +import reactors.example.HelloReactorTest; +import reactors.example.OpenMCPAppReactorTest; + +/** + * Test suite that runs all reactor tests in the project. + * This suite aggregates all reactor test classes and can be executed to run + * all tests at once for comprehensive validation. + * + *

+ * To run this suite: + * + *

+ * mvn test -Dtest=ReactorTestSuite
+ * 
+ * + *

+ * Included test classes: + *

    + *
  • {@link HelloReactorTest} - Tests for HelloUserReactor
  • + *
  • {@link CallPythonReactorTest} - Tests for CallPythonReactor
  • + *
  • {@link OpenMCPAppReactorTest} - Tests for OpenMCPAppReactor
  • + *
+ */ +@Suite +@SuiteDisplayName("Reactor Test Suite") +@SelectClasses({ + HelloReactorTest.class, + CallPythonReactorTest.class, + OpenMCPAppReactorTest.class +}) +public class ReactorTestSuite { + // This class remains empty, it is used only as a holder for the above + // annotations +} diff --git a/test/reactors/example/CallPythonReactorTest.java b/test/reactors/example/CallPythonReactorTest.java new file mode 100644 index 0000000..a27d704 --- /dev/null +++ b/test/reactors/example/CallPythonReactorTest.java @@ -0,0 +1,197 @@ +package reactors.example; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import reactors.BaseReactorTest; +import reactors.examples.CallPythonReactor; +import prerna.sablecc2.om.PixelDataType; +import prerna.sablecc2.om.ReactorKeysEnum; +import prerna.sablecc2.om.nounmeta.NounMetadata; + +/** + * Test class for CallPythonReactor functionality. + * Tests Python integration for Fibonacci number calculation. + */ +@DisplayName("CallPythonReactor Tests") +public class CallPythonReactorTest extends BaseReactorTest { + + private CallPythonReactor reactor; + private static final String FIBONACCI_MODULE = "nthFibonacci"; + private static final String FIBONACCI_FUNCTION = "nthFibonacci"; + + @BeforeEach + void setup() throws IOException { + reactor = new CallPythonReactor(); + reactor.setInsight(insight); + reactor.setNounStore(nounStore); + + // Copy the actual Python file from py directory to temp directory + Path sourcePath = Paths.get("py", "nthFibonacci.py"); + Path targetPath = tempDir.resolve("py").resolve("nthFibonacci.py"); + Files.createDirectories(targetPath.getParent()); + Files.copy(sourcePath, targetPath, StandardCopyOption.REPLACE_EXISTING); + } + + @Test + @DisplayName("Should calculate Fibonacci number for input 0") + public void testCallPythonReactor_Fibonacci0() { + // Set up mocks + int input = 0; + int expectedFibonacci = 0; + setupPyTranslatorMocks(FIBONACCI_MODULE, FIBONACCI_FUNCTION, expectedFibonacci); + + // Set up reactor with input parameter + setReactorParameter(reactor, ReactorKeysEnum.NUMERIC_VALUE.getKey(), input); + + // Execute the reactor + NounMetadata result = reactor.execute(); + + // Verify result type + assertEquals(PixelDataType.CONST_STRING, result.getNounType()); + + // Verify the result value + assertNotNull(result.getValue()); + assertEquals(expectedFibonacci, result.getValue()); + + // Verify Python translator was called correctly + verify(pyTranslator).loadPythonModuleFromFile( + eq(insight), + eq("nthFibonacci.py"), + eq(TEST_PROJECT_ID)); + verify(pyTranslator).runFunctionFromLoadedModule( + eq(insight), + eq(FIBONACCI_MODULE), + eq(FIBONACCI_FUNCTION), + anyList()); + } + + @Test + @DisplayName("Should calculate Fibonacci number for input 1") + public void testCallPythonReactor_Fibonacci1() { + // Set up mocks + int input = 1; + int expectedFibonacci = 1; + setupPyTranslatorMocks(FIBONACCI_MODULE, FIBONACCI_FUNCTION, expectedFibonacci); + + // Set up reactor with input parameter + setReactorParameter(reactor, ReactorKeysEnum.NUMERIC_VALUE.getKey(), input); + + // Execute the reactor + NounMetadata result = reactor.execute(); + + // Verify result + assertEquals(PixelDataType.CONST_STRING, result.getNounType()); + assertEquals(expectedFibonacci, result.getValue()); + } + + @Test + @DisplayName("Should calculate Fibonacci number for input 5") + public void testCallPythonReactor_Fibonacci5() { + // Set up mocks + int input = 5; + int expectedFibonacci = 5; // Fibonacci(5) = 5 + setupPyTranslatorMocks(FIBONACCI_MODULE, FIBONACCI_FUNCTION, expectedFibonacci); + + // Set up reactor with input parameter + setReactorParameter(reactor, ReactorKeysEnum.NUMERIC_VALUE.getKey(), input); + + // Execute the reactor + NounMetadata result = reactor.execute(); + + // Verify result + assertEquals(PixelDataType.CONST_STRING, result.getNounType()); + assertEquals(expectedFibonacci, result.getValue()); + } + + @Test + @DisplayName("Should calculate Fibonacci number for input 10") + public void testCallPythonReactor_Fibonacci10() { + // Set up mocks + int input = 10; + int expectedFibonacci = 55; // Fibonacci(10) = 55 + setupPyTranslatorMocks(FIBONACCI_MODULE, FIBONACCI_FUNCTION, expectedFibonacci); + + // Set up reactor with input parameter + setReactorParameter(reactor, ReactorKeysEnum.NUMERIC_VALUE.getKey(), input); + + // Execute the reactor + NounMetadata result = reactor.execute(); + + // Verify result + assertEquals(PixelDataType.CONST_STRING, result.getNounType()); + assertEquals(expectedFibonacci, result.getValue()); + } + + @Test + @DisplayName("Should handle large Fibonacci numbers") + public void testCallPythonReactor_LargeFibonacci() { + // Set up mocks + int input = 20; + int expectedFibonacci = 6765; // Fibonacci(20) = 6765 + setupPyTranslatorMocks(FIBONACCI_MODULE, FIBONACCI_FUNCTION, expectedFibonacci); + + // Set up reactor with input parameter + setReactorParameter(reactor, ReactorKeysEnum.NUMERIC_VALUE.getKey(), input); + + // Execute the reactor + NounMetadata result = reactor.execute(); + + // Verify result + assertEquals(PixelDataType.CONST_STRING, result.getNounType()); + assertEquals(expectedFibonacci, result.getValue()); + } + + @Test + @DisplayName("Should verify argument list passed to Python function") + public void testCallPythonReactor_VerifyArgumentList() { + // Set up mocks with argument capture + int input = 7; + int expectedFibonacci = 13; + + when(pyTranslator.loadPythonModuleFromFile( + eq(insight), + anyString(), + eq(TEST_PROJECT_ID))) + .thenReturn(FIBONACCI_MODULE); + + when(pyTranslator.runFunctionFromLoadedModule( + eq(insight), + eq(FIBONACCI_MODULE), + eq(FIBONACCI_FUNCTION), + anyList())) + .thenAnswer(invocation -> { + // Verify the argument list contains the correct input + @SuppressWarnings("unchecked") + List args = (List) invocation.getArgument(3); + assertEquals(1, args.size()); + assertEquals(input, args.get(0)); + return expectedFibonacci; + }); + + // Set up reactor with input parameter + setReactorParameter(reactor, ReactorKeysEnum.NUMERIC_VALUE.getKey(), input); + + // Execute the reactor + NounMetadata result = reactor.execute(); + + // Verify result + assertEquals(expectedFibonacci, result.getValue()); + } +} diff --git a/test/reactors/example/HelloReactorTest.java b/test/reactors/example/HelloReactorTest.java new file mode 100644 index 0000000..7ef6193 --- /dev/null +++ b/test/reactors/example/HelloReactorTest.java @@ -0,0 +1,89 @@ +package reactors.example; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import reactors.BaseReactorTest; +import reactors.examples.HelloUserReactor; +import prerna.sablecc2.om.PixelDataType; +import prerna.sablecc2.om.ReactorKeysEnum; +import prerna.sablecc2.om.nounmeta.NounMetadata; + +/** + * Test class for HelloUserReactor functionality. + * Tests various scenarios including default user greeting and custom name + * parameter. + */ +@DisplayName("HelloUserReactor Tests") +public class HelloReactorTest extends BaseReactorTest { + + private HelloUserReactor reactor; + + @BeforeEach + void setup() { + reactor = new HelloUserReactor(); + reactor.setInsight(insight); + reactor.setNounStore(nounStore); + } + + @Test + @DisplayName("Should return greeting with default user name when no name parameter provided") + public void testHelloUserReactor_DefaultUserName() { + // Execute the reactor + NounMetadata result = reactor.execute(); + + // Verify result type + assertEquals(PixelDataType.CONST_STRING, result.getNounType()); + + // Verify the greeting contains the test user name + String greeting = (String) result.getValue(); + assertNotNull(greeting); + assertTrue(greeting.contains(TEST_USER_NAME)); + assertTrue(greeting.contains("Hello")); + assertTrue(greeting.contains("Welcome to SEMOSS")); + } + + @Test + @DisplayName("Should return greeting with custom name when name parameter provided") + public void testHelloUserReactor_CustomName() { + // Set up reactor with custom name parameter + String customName = "Alice"; + setReactorParameter(reactor, ReactorKeysEnum.NAME.getKey(), customName); + + // Execute the reactor + NounMetadata result = reactor.execute(); + + // Verify result type + assertEquals(PixelDataType.CONST_STRING, result.getNounType()); + + // Verify the greeting contains the custom name + String greeting = (String) result.getValue(); + assertNotNull(greeting); + assertTrue(greeting.contains(customName)); + assertTrue(greeting.contains("Hello")); + assertTrue(greeting.contains("Welcome to SEMOSS")); + } + + @Test + @DisplayName("Should handle empty string name parameter") + public void testHelloUserReactor_EmptyName() { + // Set up reactor with empty name parameter + setReactorParameter(reactor, ReactorKeysEnum.NAME.getKey(), ""); + + // Execute the reactor + NounMetadata result = reactor.execute(); + + // Verify result type + assertEquals(PixelDataType.CONST_STRING, result.getNounType()); + + // Verify the greeting is created (with empty name in this case) + String greeting = (String) result.getValue(); + assertNotNull(greeting); + assertTrue(greeting.contains("Hello")); + } +} diff --git a/test/reactors/example/OpenMCPAppReactorTest.java b/test/reactors/example/OpenMCPAppReactorTest.java new file mode 100644 index 0000000..325e679 --- /dev/null +++ b/test/reactors/example/OpenMCPAppReactorTest.java @@ -0,0 +1,108 @@ +package reactors.example; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import reactors.BaseReactorTest; +import reactors.examples.OpenMCPAppReactor; +import prerna.sablecc2.om.PixelDataType; +import prerna.sablecc2.om.nounmeta.NounMetadata; + +/** + * Test class for OpenMCPAppReactor functionality. + * Tests the MCP App interface opening reactor. + */ +@DisplayName("OpenMCPAppReactor Tests") +public class OpenMCPAppReactorTest extends BaseReactorTest { + + private OpenMCPAppReactor reactor; + + @BeforeEach + void setup() { + reactor = new OpenMCPAppReactor(); + reactor.setInsight(insight); + reactor.setNounStore(nounStore); + } + + @Test + @DisplayName("Should return placeholder message indicating auto-execute not implemented") + public void testOpenMCPAppReactor_ReturnsPlaceholderMessage() { + // Execute the reactor + NounMetadata result = reactor.execute(); + + // Verify result type + assertEquals(PixelDataType.CONST_STRING, result.getNounType()); + + // Verify the result contains expected message + String message = (String) result.getValue(); + assertNotNull(message); + assertTrue(message.contains("auto-execute response")); + assertTrue(message.contains("not yet been implemented")); + } + + @Test + @DisplayName("Should return exact expected placeholder message") + public void testOpenMCPAppReactor_ExactMessage() { + // Execute the reactor + NounMetadata result = reactor.execute(); + + // Verify exact message + String expectedMessage = "This MCP tool's auto-execute response has not yet been implemented."; + assertEquals(expectedMessage, result.getValue()); + } + + @Test + @DisplayName("Should not require any input parameters") + public void testOpenMCPAppReactor_NoParametersRequired() { + // Verify reactor can execute without any parameters set + NounMetadata result = reactor.execute(); + + // Should still return a valid result + assertNotNull(result); + assertEquals(PixelDataType.CONST_STRING, result.getNounType()); + } + + @Test + @DisplayName("Should return reactor description for MCP tool") + public void testOpenMCPAppReactor_Description() { + // Get reactor description + String description = reactor.getReactorDescription(); + + // Verify description is present and contains expected content + assertNotNull(description); + assertTrue(description.contains("SEMOSS Template application")); + assertTrue(description.contains("interact")); + } + + @Test + @DisplayName("Should return expected exact description") + public void testOpenMCPAppReactor_ExactDescription() { + // Get reactor description + String description = reactor.getReactorDescription(); + + // Verify exact description + String expectedDescription = "This tool allows the user to interact with the SEMOSS Template application."; + assertEquals(expectedDescription, description); + } + + @Test + @DisplayName("Should execute successfully multiple times") + public void testOpenMCPAppReactor_MultipleExecutions() { + // Execute the reactor multiple times + NounMetadata result1 = reactor.execute(); + NounMetadata result2 = reactor.execute(); + NounMetadata result3 = reactor.execute(); + + // All results should be identical + assertEquals(result1.getValue(), result2.getValue()); + assertEquals(result2.getValue(), result3.getValue()); + assertEquals(PixelDataType.CONST_STRING, result1.getNounType()); + assertEquals(PixelDataType.CONST_STRING, result2.getNounType()); + assertEquals(PixelDataType.CONST_STRING, result3.getNounType()); + } +}