diff --git a/.github/workflows/create-releases.yml b/.github/workflows/create-releases.yml index f7b805807..b7c042468 100644 --- a/.github/workflows/create-releases.yml +++ b/.github/workflows/create-releases.yml @@ -40,10 +40,6 @@ jobs: run: | ./gradlew :openai-java-core:compileJava :openai-java-core:compileTestJava -x test - - name: Run the Prism server - run: | - ./scripts/mock --daemon - - name: Setup GraalVM uses: graalvm/setup-graalvm@v1 with: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b2884477b..0c074129d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -87,18 +87,12 @@ JAR files will be available in each module's `build/libs/` directory. Most tests require [our mock server](https://github.com/stoplightio/prism) to be running against the OpenAPI spec to work. -The test script will automatically start the mock server for you (if it's not already running) and run the tests against it: +The test script will automatically start the mock server for you and run the tests against it: ```sh $ ./scripts/test ``` -You can also manually start the mock server if you want to run tests repeatedly: - -```sh -$ ./scripts/mock -``` - Then run the tests: ```sh diff --git a/openai-java-core/build.gradle.kts b/openai-java-core/build.gradle.kts index 7c7d6d0eb..abeb5621c 100644 --- a/openai-java-core/build.gradle.kts +++ b/openai-java-core/build.gradle.kts @@ -43,6 +43,8 @@ dependencies { testImplementation("org.mockito:mockito-core:5.14.2") testImplementation("org.mockito:mockito-junit-jupiter:5.14.2") testImplementation("org.mockito.kotlin:mockito-kotlin:4.1.0") + testImplementation("org.testcontainers:testcontainers:1.19.8") + testImplementation("org.testcontainers:junit-jupiter:1.19.8") } if (project.hasProperty("graalvmAgent")) { diff --git a/openai-java-core/src/test/kotlin/com/openai/TestServerExtension.kt b/openai-java-core/src/test/kotlin/com/openai/TestServerExtension.kt index 7dfdef2b1..ceb13557b 100644 --- a/openai-java-core/src/test/kotlin/com/openai/TestServerExtension.kt +++ b/openai-java-core/src/test/kotlin/com/openai/TestServerExtension.kt @@ -1,39 +1,143 @@ package com.openai +import java.io.File import java.lang.RuntimeException -import java.net.URL +import java.time.Duration import org.junit.jupiter.api.extension.BeforeAllCallback import org.junit.jupiter.api.extension.ConditionEvaluationResult import org.junit.jupiter.api.extension.ExecutionCondition import org.junit.jupiter.api.extension.ExtensionContext +import org.testcontainers.containers.GenericContainer +import org.testcontainers.containers.wait.strategy.Wait +import org.testcontainers.utility.DockerImageName +import org.testcontainers.utility.MountableFile class TestServerExtension : BeforeAllCallback, ExecutionCondition { - override fun beforeAll(context: ExtensionContext?) { - try { - URL(BASE_URL).openConnection().connect() - } catch (e: Exception) { - throw RuntimeException( - """ - The test suite will not run without a mock Prism server running against your OpenAPI spec. + companion object { + private const val INTERNAL_PORT = 4010 // Port inside the container - You can set the environment variable `SKIP_MOCK_TESTS` to `true` to skip running any tests - that require the mock server. + val BASE_URL: String + get() = "http://${prismContainer.host}:${prismContainer.getMappedPort(INTERNAL_PORT)}" - To fix: + const val SKIP_TESTS_ENV: String = "SKIP_MOCK_TESTS" + private const val NODEJS_IMAGE = "node:22" + private const val PRISM_CLI_VERSION = "5.8.5" + private const val API_SPEC_PATH = "/app/openapi.yml" // Path inside the container - 1. Install Prism (requires Node 16+): + // Track if the container has been started + private var containerStarted = false + + private fun getOpenApiSpecPath(): String { + // First check environment variable + val envPath = System.getenv("OPENAPI_SPEC_PATH") + if (envPath != null) { + return envPath + } + + // Try to read from .stats.yml file + try { + val statsFile = File("../.stats.yml") + if (statsFile.exists()) { + val content = statsFile.readText() + val urlLine = content.lines().find { it.startsWith("openapi_spec_url:") } + if (urlLine != null) { + val url = urlLine.substringAfter("openapi_spec_url:").trim() + if (url.isNotEmpty()) { + return url + } + } + } + } catch (e: Exception) { + println( + "Could not read .stats.yml fails, fall back to default. Error is: ${e.message}" + ) + } + return "/tmp/openapi.yml" + } + + private val prismContainer: GenericContainer<*> by lazy { + val apiSpecPath = getOpenApiSpecPath() + println("Using OpenAPI spec path: $apiSpecPath") + val isUrl = apiSpecPath.startsWith("http://") || apiSpecPath.startsWith("https://") + + // Create container with or without copying the file based on whether apiSpecPath is a + // URL + val container = + GenericContainer(DockerImageName.parse(NODEJS_IMAGE)) + .withExposedPorts(INTERNAL_PORT) + .withCommand( + "npm", + "exec", + "--package=@stainless-api/prism-cli@$PRISM_CLI_VERSION", + "--", + "prism", + "mock", + apiSpecPath, + "--host", + "0.0.0.0", + "--port", + INTERNAL_PORT.toString(), + ) + .withReuse(true) + + // Only copy the file to the container if apiSpecPath is a local file + if (!isUrl) { + try { + val file = File(apiSpecPath) + if (file.exists()) { + container.withCopyToContainer( + MountableFile.forHostPath(apiSpecPath), + API_SPEC_PATH, + ) + } else { + println("OpenAPI spec file not found at: $apiSpecPath") + throw RuntimeException("OpenAPI spec file not found at: $apiSpecPath") + } + } catch (e: Exception) { + println("Error reading OpenAPI spec file: ${e.message}") + throw RuntimeException("Error reading OpenAPI spec file: $apiSpecPath", e) + } + } + + // Add waiting strategy + container.waitingFor( + Wait.forLogMessage(".*Prism is listening.*", 1) + .withStartupTimeout(Duration.ofSeconds(300)) + ) - With npm: - $ npm install -g @stoplight/prism-cli + // Start the container here once during lazy initialization + container.start() + containerStarted = true + println( + "Prism container started at: ${container.host}:${container.getMappedPort(INTERNAL_PORT)}" + ) - With yarn: - $ yarn global add @stoplight/prism-cli + container + } - 2. Run the mock server + // Method to ensure container is started, can be called from beforeAll + fun ensureContainerStarted() { + if (!containerStarted) { + // This will trigger lazy initialization and start the container + prismContainer + } + } + } - To run the server, pass in the path of your OpenAPI spec to the prism command: - $ prism mock path/to/your.openapi.yml + override fun beforeAll(context: ExtensionContext?) { + try { + // Use the companion method to ensure container is started only once + ensureContainerStarted() + } catch (e: Exception) { + throw RuntimeException( + """ + Failed to connect to Prism mock server running in TestContainer. + + You can set the environment variable `SKIP_MOCK_TESTS` to `true` to skip running any tests + that require the mock server. + + You may also need to set `OPENAPI_SPEC_PATH` to the path of your OpenAPI spec file. """ .trimIndent(), e, @@ -52,11 +156,4 @@ class TestServerExtension : BeforeAllCallback, ExecutionCondition { ) } } - - companion object { - - val BASE_URL = System.getenv("TEST_API_BASE_URL") ?: "http://localhost:4010" - - const val SKIP_TESTS_ENV: String = "SKIP_MOCK_TESTS" - } } diff --git a/scripts/mock b/scripts/mock deleted file mode 100755 index d2814ae6a..000000000 --- a/scripts/mock +++ /dev/null @@ -1,41 +0,0 @@ -#!/usr/bin/env bash - -set -e - -cd "$(dirname "$0")/.." - -if [[ -n "$1" && "$1" != '--'* ]]; then - URL="$1" - shift -else - URL="$(grep 'openapi_spec_url' .stats.yml | cut -d' ' -f2)" -fi - -# Check if the URL is empty -if [ -z "$URL" ]; then - echo "Error: No OpenAPI spec path/url provided or found in .stats.yml" - exit 1 -fi - -echo "==> Starting mock server with URL ${URL}" - -# Run prism mock on the given spec -if [ "$1" == "--daemon" ]; then - npm exec --package=@stainless-api/prism-cli@5.8.5 -- prism mock "$URL" &> .prism.log & - - # Wait for server to come online - echo -n "Waiting for server" - while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do - echo -n "." - sleep 0.1 - done - - if grep -q "✖ fatal" ".prism.log"; then - cat .prism.log - exit 1 - fi - - echo -else - npm exec --package=@stainless-api/prism-cli@5.8.5 -- prism mock "$URL" -fi diff --git a/scripts/test b/scripts/test index 6b750a74e..0f93c0bd8 100755 --- a/scripts/test +++ b/scripts/test @@ -4,53 +4,5 @@ set -e cd "$(dirname "$0")/.." -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[0;33m' -NC='\033[0m' # No Color - -function prism_is_running() { - curl --silent "http://localhost:4010" >/dev/null 2>&1 -} - -kill_server_on_port() { - pids=$(lsof -t -i tcp:"$1" || echo "") - if [ "$pids" != "" ]; then - kill "$pids" - echo "Stopped $pids." - fi -} - -function is_overriding_api_base_url() { - [ -n "$TEST_API_BASE_URL" ] -} - -if ! is_overriding_api_base_url && ! prism_is_running ; then - # When we exit this script, make sure to kill the background mock server process - trap 'kill_server_on_port 4010' EXIT - - # Start the dev server - ./scripts/mock --daemon -fi - -if is_overriding_api_base_url ; then - echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}" - echo -elif ! prism_is_running ; then - echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server" - echo -e "running against your OpenAPI spec." - echo - echo -e "To run the server, pass in the path or url of your OpenAPI" - echo -e "spec to the prism command:" - echo - echo -e " \$ ${YELLOW}npm exec --package=@stoplight/prism-cli@~5.3.2 -- prism mock path/to/your.openapi.yml${NC}" - echo - - exit 1 -else - echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}" - echo -fi - echo "==> Running tests" ./gradlew test