Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 51 additions & 8 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,64 @@ on:
branches: [ "main" ]

jobs:
test:
name: Run Tests
unit-tests:
name: Unit Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Java 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: 'maven'

- name: Run Maven Tests

- name: Run Unit Tests
run: make test-unit

integration-tests:
name: Integration Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Java 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: 'maven'

- name: Start test infrastructure
run: make docker-up

- name: Run Integration Tests
env:
RHESIS_API_KEY: ${{ secrets.RHESIS_API_KEY }}
RHESIS_BASE_URL: ${{ secrets.RHESIS_BASE_URL }}
run: make test
RHESIS_API_KEY: rh-local-token
RHESIS_BASE_URL: http://localhost:10003
run: make test-integration

- name: Show backend logs on failure
if: failure()
run: docker compose -f docker-compose.test.yml logs test-backend

- name: Stop test infrastructure
if: always()
run: make docker-clean

lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Java 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: 'maven'

- name: Run Lint
run: make lint
16 changes: 15 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: format lint check test test-unit test-integration build clean
.PHONY: format lint check test test-unit test-integration build clean docker-up docker-down docker-clean

# Format code using spotless (fixes the formatting errors you're seeing)
format:
Expand Down Expand Up @@ -29,3 +29,17 @@ build:
# Clean the target directory
clean:
mvn clean

# Docker management for integration tests
docker-up:
docker compose -f docker-compose.test.yml up -d --wait
@echo "Seeding project scope on test token..."
@docker compose -f docker-compose.test.yml exec -T test-postgres \
psql -U rhesis-user -d rhesis-db -c \
"UPDATE token SET project_id = (SELECT id FROM project LIMIT 1);"

docker-down:
docker compose -f docker-compose.test.yml down

docker-clean:
docker compose -f docker-compose.test.yml down -v
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ The Rhesis Java SDK provides programmatic access to the Rhesis testing platform:
- **Access Test Sets**: Browse and load curated test sets across multiple domains and use cases
- **Generate Test Scenarios**: Create custom test sets from prompts, requirements, or domain knowledge natively using local LLM models
- **Execute Test Runs**: Trigger test set runs against your endpoints in parallel or sequential mode
- **Analytics & Stats**: Typed stats for test runs and test results — pass rates by metric, behavior, category, topic, and timeline trends
- **Analytics & Stats**: Typed stats for test runs and test results — pass rates by metric, requirement, category, topic, and timeline trends
- **Manage Metrics & Tests**: Add/remove metrics on test sets, associate/disassociate tests, rescore previous runs
- **Seamless Integration**: Integrate testing into your Java CI/CD pipeline and development workflow
- **Comprehensive Coverage**: Scale your testing from dozens to thousands of scenarios
Expand Down Expand Up @@ -259,7 +259,7 @@ public class Main {

### Generating Custom Test Sets 🛠️

If none of the existing test sets fit your needs, you can generate your own. You can check out [app.rhesis.ai](http://app.rhesis.ai). There you can define requirements, scenarios and behaviors.
If none of the existing test sets fit your needs, you can generate your own. You can check out [app.rhesis.ai](http://app.rhesis.ai). There you can define requirements, scenarios and test configurations.

### Examples 💡

Expand All @@ -284,7 +284,7 @@ Looking for more detailed examples? Check out the full [Examples README](src/tes

**Analytics & Stats**
- [Test Run Stats](src/test/java/ai/rhesis/sdk/examples/TestRunStatsExample.java) — Overall summary, status distribution, most-run test sets, timeline, filtered queries
- [Test Result Stats](src/test/java/ai/rhesis/sdk/examples/TestResultStatsExample.java) — Pass rates by metric, behavior, category, and topic; per-run summaries and timeline trends
- [Test Result Stats](src/test/java/ai/rhesis/sdk/examples/TestResultStatsExample.java) — Pass rates by metric, requirement, category, and topic; per-run summaries and timeline trends

You can run any example from the command line using Maven. Make sure your `RHESIS_API_KEY` is set in your environment (it will be automatically picked up from a `.env` file at the root of the project if one exists):

Expand Down
90 changes: 90 additions & 0 deletions docker-compose.test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
# Test infrastructure for Java SDK integration tests.
# Mirrors the approach in rhesis-ai/rhesis (tests/docker-compose.test.yml).
#
# Usage:
# docker compose -f docker-compose.test.yml up -d --wait
# make test-integration
# docker compose -f docker-compose.test.yml down -v

x-database-config: &database-config
DB_NAME: rhesis-db
APP_DB_USER: rhesis-user
APP_DB_PASS: rhesis-password
DB_PORT: "5432"
DB_DRIVER: postgresql
DB_HOST: test-postgres
DB_ENCRYPTION_KEY: Zb21wZbPsUpb-c2JKj8uMugk767pWXHFTsjocd0Orac=

x-redis-config: &redis-config
BROKER_URL: redis://:rhesis-redis-pass@test-redis:6379/0
CELERY_RESULT_BACKEND: redis://:rhesis-redis-pass@test-redis:6379/1

x-backend-config: &backend-config
FRONTEND_URL: "http://localhost:3000"
JWT_SECRET_KEY: ci-test-jwt-secret
SESSION_SECRET_KEY: ci-test-session-secret
LOG_LEVEL: DEBUG
RHESIS_CONNECTOR_DISABLED: "true"
API_BASE_URL: "http://localhost:10003"
OTEL_RHESIS_TELEMETRY_ENABLED: "false"
QUICK_START: "true"
BACKEND_ENV: local
ENABLE_RHESIS_KEY: "true"

services:
test-postgres:
image: pgvector/pgvector:pg16
environment:
POSTGRES_DB: rhesis-db
POSTGRES_USER: rhesis-user
POSTGRES_PASSWORD: rhesis-password
ports:
- "10001:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U rhesis-user -d rhesis-db"]
interval: 10s
timeout: 5s
retries: 5
networks:
- test-network
tmpfs:
- /var/lib/postgresql/data

test-redis:
image: redis:7-alpine
command: redis-server --requirepass rhesis-redis-pass
ports:
- "10002:6379"
healthcheck:
test: ["CMD", "redis-cli", "-a", "rhesis-redis-pass", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- test-network
tmpfs:
- /data

test-backend:
image: ghcr.io/rhesis-ai/backend:latest
environment:
<<: [*database-config, *redis-config, *backend-config]
ports:
- "10003:8080"
depends_on:
test-postgres:
condition: service_healthy
test-redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 10s
timeout: 5s
retries: 10
start_period: 60s
networks:
- test-network

networks:
test-network:
driver: bridge
7 changes: 7 additions & 0 deletions src/main/java/ai/rhesis/sdk/RhesisClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import ai.rhesis.sdk.clients.EndpointClient;
import ai.rhesis.sdk.clients.FileClient;
import ai.rhesis.sdk.clients.InsightsClient;
import ai.rhesis.sdk.clients.ProjectClient;
import ai.rhesis.sdk.clients.TestClient;
import ai.rhesis.sdk.clients.TestResultClient;
Expand All @@ -24,6 +25,7 @@ public class RhesisClient {
private final TestRunClient testRuns;
private final TestResultClient testResults;
private final FileClient files;
private final InsightsClient insights;

RhesisClient(String baseUrl, String apiKey) {
this.httpClient = new InternalHttpClient(baseUrl, apiKey);
Expand All @@ -36,6 +38,7 @@ public class RhesisClient {
this.testRuns = new TestRunClient(this.httpClient);
this.testResults = new TestResultClient(this.httpClient);
this.files = new FileClient(this.httpClient);
this.insights = new InsightsClient(this.httpClient);
}

public static RhesisClientBuilder builder() {
Expand Down Expand Up @@ -96,4 +99,8 @@ public TestResultClient testResults() {
public FileClient files() {
return files;
}

public InsightsClient insights() {
return insights;
}
}
130 changes: 130 additions & 0 deletions src/main/java/ai/rhesis/sdk/clients/InsightsClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package ai.rhesis.sdk.clients;

import ai.rhesis.sdk.entities.InsightsIdsResponse;
import ai.rhesis.sdk.entities.InsightsQuery;
import ai.rhesis.sdk.entities.InsightsResponse;
import ai.rhesis.sdk.http.InternalHttpClient;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;

public class InsightsClient {
private final InternalHttpClient httpClient;

public InsightsClient(InternalHttpClient httpClient) {
this.httpClient = httpClient;
}

/**
* Run an insights aggregation query.
*
* @param query the insights query (entity, group_by, measures, filters, date range)
* @return InsightsResponse with entity, dimensions, measures, and rows
*/
public InsightsResponse get(InsightsQuery query) {
StringBuilder path = new StringBuilder("/insights/?entity=");
path.append(encode(query.entity()));
appendList(path, "group_by", query.groupBy());
appendList(path, "measures", query.measures());
appendFilters(path, query.filters());
appendDateRange(path, query);
return httpClient.get(path.toString(), InsightsResponse.class);
}

/**
* Run an insights aggregation query.
*
* @param entity registry entity: "test_result", "metric", "test_run", or "test"
* @param groupBy dimensions to group by (e.g. "requirement", "category", "topic")
* @param measures measures to compute (e.g. "count", "pass_rate", "passed", "failed")
* @param filters optional filter parameters keyed by filter name
* @return InsightsResponse with entity, dimensions, measures, and rows
*/
public InsightsResponse get(
String entity,
List<String> groupBy,
List<String> measures,
Map<String, List<String>> filters) {
InsightsQuery.Builder builder = InsightsQuery.builder(entity);
if (groupBy != null) builder.groupBy(groupBy);
if (measures != null) builder.measures(measures);
if (filters != null) builder.filters(filters);
return get(builder.build());
}

/**
* Run an insights aggregation query with default measures (count).
*
* @param entity registry entity
* @param groupBy dimensions to group by
* @return InsightsResponse
*/
public InsightsResponse get(String entity, List<String> groupBy) {
return get(entity, groupBy, List.of("count"), null);
}

/**
* Resolve distinct entity IDs matching insights filters.
*
* @param query the insights query (entity, filters, date range)
* @param outcome "pass", "fail", or "all"
* @return InsightsIdsResponse with entity and ids
*/
public InsightsIdsResponse ids(InsightsQuery query, String outcome) {
StringBuilder path = new StringBuilder("/insights/ids?entity=");
path.append(encode(query.entity()));
path.append("&outcome=").append(encode(outcome != null ? outcome : "all"));
appendFilters(path, query.filters());
appendDateRange(path, query);
return httpClient.get(path.toString(), InsightsIdsResponse.class);
}

/**
* Resolve distinct entity IDs matching insights filters.
*
* @param entity registry entity
* @param outcome "pass", "fail", or "all"
* @param filters optional filter parameters
* @return InsightsIdsResponse with entity and ids
*/
public InsightsIdsResponse ids(String entity, String outcome, Map<String, List<String>> filters) {
InsightsQuery.Builder builder = InsightsQuery.builder(entity);
if (filters != null) builder.filters(filters);
return ids(builder.build(), outcome);
}

private static void appendList(StringBuilder path, String param, List<String> values) {
if (values != null) {
for (String value : values) {
path.append("&").append(param).append("=").append(encode(value));
}
}
}

private static void appendFilters(StringBuilder path, Map<String, List<String>> filters) {
if (filters != null) {
for (Map.Entry<String, List<String>> entry : filters.entrySet()) {
for (String value : entry.getValue()) {
path.append("&").append(encode(entry.getKey())).append("=").append(encode(value));
}
}
}
}

private static void appendDateRange(StringBuilder path, InsightsQuery query) {
if (query.months() != null) {
path.append("&months=").append(query.months());
}
if (query.startDate() != null) {
path.append("&start_date=").append(encode(query.startDate()));
}
if (query.endDate() != null) {
path.append("&end_date=").append(encode(query.endDate()));
}
}

private static String encode(String value) {
return URLEncoder.encode(value, StandardCharsets.UTF_8);
}
}
2 changes: 1 addition & 1 deletion src/main/java/ai/rhesis/sdk/clients/TestClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public Test create(Test test) {
? new Test(
test.id(),
test.testConfiguration(),
test.behavior(),
test.requirement(),
test.category(),
test.topic(),
test.testType(),
Expand Down
Loading
Loading