Skip to content
Open
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
6 changes: 6 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ help:
@echo " test-integration - Run all integration tests"
@echo " test-integration-csharp - Run C# integration tests"
@echo " test-integration-go - Run Go integration tests"
@echo " test-integration-java - Run Java integration tests"
@echo " test-integration-nodejs - Run NodeJS integration tests"
@echo " generate - Generate all code (API clients, docs, schema)"
@echo " generate-api - Generate API clients from OpenAPI specs"
Expand Down Expand Up @@ -67,6 +68,11 @@ test-integration-angular:
@echo "Running Angular integration test with Dagger..."
@go run ./test/integration/cmd/angular/run.go

.PHONY: test-integration-java
test-integration-java:
@echo "Running Java integration test with Dagger..."
@go run ./test/integration/cmd/java/run.go

.PHONY: test-integration
test-integration:
@echo "Running all integration tests with Dagger..."
Expand Down
95 changes: 95 additions & 0 deletions test/integration/cmd/java/run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package main

import (
"context"
"fmt"
"os"
"path/filepath"

"dagger.io/dagger"
"github.com/open-feature/cli/test/integration"
)

// Test implements the integration test for the Java generator
type Test struct {
// ProjectDir is the absolute path to the root of the project
ProjectDir string
// TestDir is the absolute path to the test directory
TestDir string
}

// New creates a new Test
func New(projectDir, testDir string) *Test {
return &Test{
ProjectDir: projectDir,
TestDir: testDir,
}
}

// Run executes the Java integration test using Dagger
func (t *Test) Run(ctx context.Context, client *dagger.Client) (*dagger.Container, error) {
// Source code container
source := client.Host().Directory(t.ProjectDir)
testFiles := client.Host().Directory(t.TestDir, dagger.HostDirectoryOpts{
Include: []string{"pom.xml", "src/**/*.java"},
})

// Build the CLI
cli := client.Container().
From("golang:1.24-alpine").
WithExec([]string{"apk", "add", "--no-cache", "git"}).

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

It's generally recommended to specify a full version tag for base images in production or CI environments to ensure reproducibility and prevent unexpected changes if golang:1.24-alpine is updated to a newer minor version that introduces breaking changes. For example, golang:1.24.2-alpine.

WithDirectory("/src", source).
WithWorkdir("/src").
WithExec([]string{"go", "mod", "tidy"}).
WithExec([]string{"go", "mod", "download"}).
WithExec([]string{"go", "build", "-o", "cli", "./cmd/openfeature"})

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The build command for the CLI is missing the ldflags to inject version information. This is inconsistent with the build target in the Makefile and general repository rules about including commit hashes in builds.

While this seems to be a pre-existing pattern in other integration tests, it would be a good improvement to start aligning them. You could pass the commit hash from the host (e.g., from an environment variable) and use it to construct the ldflags for the build.

For example:

commit := os.Getenv("COMMIT_SHA") // assuming it's passed from the calling script
ldflags := fmt.Sprintf("-s -w -X 'main.commit=%s'", commit)
// ...
cli := client.Container().
    // ...
    WithExec([]string{"go", "build", "-ldflags", ldflags, "-o", "cli", "./cmd/openfeature"})

This would require changes in how the test is invoked to pass the commit hash.

References
  1. Use the full commit hash for release builds and the short commit hash for development builds to differentiate them at a glance.


// Generate Java client
generated := cli.WithExec([]string{
"./cli", "generate", "java",
"--manifest=/src/sample/sample_manifest.json",
"--output=/tmp/generated",
"--package-name=dev.openfeature.generated",
})

// Get generated files
generatedFiles := generated.Directory("/tmp/generated")

// Test Java compilation with the generated files
javaContainer := client.Container().
From("maven:3.9-eclipse-temurin-21-alpine").

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the Go base image, it's good practice to pin the Maven image to a specific patch version (e.g., maven:3.9.6-eclipse-temurin-21-alpine) to avoid unexpected behavior from minor version updates.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To align with the goal of supporting Java 11 as the minimum version (as mentioned in the PR description and the suggested change in pom.xml), the Docker image used for the test should also be a Java 11-based image.

Suggested change
From("maven:3.9-eclipse-temurin-21-alpine").
From("maven:3.9-eclipse-temurin-11-alpine").

WithWorkdir("/app").
WithDirectory("/app", testFiles).
WithDirectory("/app/src/main/java/dev/openfeature/generated", generatedFiles).
WithExec([]string{"mvn", "clean", "compile", "-B", "-q"}).
WithExec([]string{"mvn", "exec:java", "-Dexec.mainClass=dev.openfeature.Main", "-q"})

return javaContainer, nil
}

// Name returns the name of the integration test
func (t *Test) Name() string {
return "java"
}

func main() {
ctx := context.Background()

// Get project root
projectDir, err := os.Getwd()
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to get project dir: %v\n", err)
os.Exit(1)
}

// Get test directory
testDir := filepath.Join(projectDir, "test/java-integration")

// Create and run the Java integration test
test := New(projectDir, testDir)

if err := integration.RunTest(ctx, test); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
9 changes: 9 additions & 0 deletions test/integration/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,15 @@ func main() {
os.Exit(1)
}

// Run the Java integration test
javaCmd := exec.Command("go", "run", "github.com/open-feature/cli/test/integration/cmd/java")
javaCmd.Stdout = os.Stdout
javaCmd.Stderr = os.Stderr
if err := javaCmd.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error running Java integration test: %v\n", err)
os.Exit(1)
}
Comment on lines +49 to +55

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

While this works, the pattern for running each integration test is duplicated multiple times in this main function. This makes the file harder to maintain and more error-prone when adding new tests. Consider refactoring to use a loop to execute the tests.

For example, the main function could be simplified to:

func main() {
	fmt.Println("=== Running all integration tests ===")

	tests := []string{
		"csharp",
		"go",
		"nodejs",
		"angular",
		"java",
	}

	for _, test := range tests {
		cmd := exec.Command("go", "run", "github.com/open-feature/cli/test/integration/cmd/"+test)
		cmd.Stdout = os.Stdout
		cmd.Stderr = os.Stderr
		if err := cmd.Run(); err != nil {
			fmt.Fprintf(os.Stderr, "Error running %s integration test: %v\n", test, err)
			os.Exit(1)
		}
	}

	fmt.Println("=== All integration tests passed successfully ===")
}


// Add more tests here as they are available

fmt.Println("=== All integration tests passed successfully ===")
Expand Down
48 changes: 48 additions & 0 deletions test/java-integration/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>dev.openfeature</groupId>
<artifactId>cli-java-integration-test</artifactId>
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
Comment on lines +12 to +13

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The PR description states that the minimum Java version is 11, but this pom.xml is configured for Java 21. To align with the documentation and ensure the generated code is compatible with the stated minimum version, please update the compiler source and target versions to 11.

Suggested change
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>

Comment on lines +12 to +13

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The PR description mentions that the Java generator targets a minimum version of Java 11. However, the pom.xml is configured for Java 21. To ensure the integration test validates compatibility with the intended minimum version, you should update the compiler source and target to 11. You will also need to update the Docker image in test/integration/cmd/java/run.go to use a Java 11 image.

Suggested change
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>

<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<openfeature.sdk.version>1.14.0</openfeature.sdk.version>
<maven-compiler-plugin.version>3.11.0</maven-compiler-plugin.version>
<exec-maven-plugin.version>3.1.0</exec-maven-plugin.version>
</properties>

<dependencies>
<!-- OpenFeature Java SDK -->
<dependency>
<groupId>dev.openfeature</groupId>
<artifactId>sdk</artifactId>
<version>${openfeature.sdk.version}</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>${maven-compiler-plugin.version}</version>
<configuration>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Consider using a property for the exec-maven-plugin version for better maintainability and consistency.

<artifactId>exec-maven-plugin</artifactId>
<version>${exec-maven-plugin.version}</version>
<configuration>
<mainClass>dev.openfeature.Main</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
103 changes: 103 additions & 0 deletions test/java-integration/src/main/java/dev/openfeature/Main.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package dev.openfeature;

import dev.openfeature.generated.*;
import dev.openfeature.sdk.*;
import dev.openfeature.sdk.providers.memory.Flag;
import dev.openfeature.sdk.providers.memory.InMemoryProvider;

import java.util.Map;

public class Main {
public static void main(String[] args) {
try {
run();
System.out.println("Generated Java code compiles successfully!");
} catch (Exception e) {
System.err.println("Error: " + e.getMessage());
e.printStackTrace();
System.exit(1);
}
}

private static void run() throws Exception {
// Set up the in-memory provider with test flags
Map<String, Object> themeConfig = Map.of(
"primaryColor", "#007bff",
"secondaryColor", "#6c757d"
);

Map<String, Flag<?>> flags = Map.of(
"discountPercentage", Flag.builder()
.variant("default", 0.15)
.defaultVariant("default")
.build(),
"enableFeatureA", Flag.builder()
.variant("default", false)
.defaultVariant("default")
.build(),
"greetingMessage", Flag.builder()
.variant("default", "Hello there!")
.defaultVariant("default")
.build(),
"usernameMaxLength", Flag.builder()
.variant("default", 50)
.defaultVariant("default")
.build(),
"themeCustomization", Flag.builder()
.variant("default", new Value(themeConfig))
.defaultVariant("default")
.build()
);

InMemoryProvider provider = new InMemoryProvider(flags);

// Set the provider
OpenFeatureAPI.getInstance().setProviderAndWait(provider);

Client client = OpenFeatureAPI.getInstance().getClient();
MutableContext evalContext = new MutableContext();

// Use the generated code for all flag evaluations
Boolean enableFeatureA = EnableFeatureA.value(client, evalContext);
System.out.println("enableFeatureA: " + enableFeatureA);
FlagEvaluationDetails<Boolean> enableFeatureADetails = EnableFeatureA.valueWithDetails(client, evalContext);
if (enableFeatureADetails.getErrorCode() != null) {
throw new Exception("Error evaluating boolean flag: " + enableFeatureADetails.getFlagKey());
}
Comment on lines 64 to 66

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The error message "Error evaluating boolean flag" is generic. It would be more helpful to include the flag key in the error message to quickly identify which flag caused the issue, especially when dealing with multiple flags.

Suggested change
if (enableFeatureADetails.getErrorCode() != null) {
throw new Exception("Error evaluating boolean flag");
}
if (enableFeatureADetails.getErrorCode() != null) {
throw new Exception("Error evaluating boolean flag: " + enableFeatureADetails.getFlagKey());
}

Comment on lines +63 to +66

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This error-checking logic is repeated for each flag type evaluation. To improve code readability and maintainability, you could extract this into a private helper method.

For example, you could add this method to the Main class:

private static <T> void checkDetails(FlagEvaluationDetails<T> details, String flagType) throws Exception {
    if (details.getErrorCode() != null) {
        throw new Exception("Error evaluating " + flagType + " flag: " + details.getFlagKey());
    }
}

Then you can replace this block and similar ones with a single call to this new helper method.

Suggested change
FlagEvaluationDetails<Boolean> enableFeatureADetails = EnableFeatureA.valueWithDetails(client, evalContext);
if (enableFeatureADetails.getErrorCode() != null) {
throw new Exception("Error evaluating boolean flag: " + enableFeatureADetails.getFlagKey());
}
checkDetails(EnableFeatureA.valueWithDetails(client, evalContext), "boolean");


Double discount = DiscountPercentage.value(client, evalContext);
System.out.printf("Discount Percentage: %.2f%n", discount);
FlagEvaluationDetails<Double> discountDetails = DiscountPercentage.valueWithDetails(client, evalContext);
if (discountDetails.getErrorCode() != null) {
throw new Exception("Failed to get discount for flag: " + discountDetails.getFlagKey());
}
Comment on lines 71 to 73

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the boolean flag, including the flag key in the error message "Failed to get discount" would improve debugging by pinpointing the exact flag that failed.

Suggested change
if (discountDetails.getErrorCode() != null) {
throw new Exception("Failed to get discount");
}
if (discountDetails.getErrorCode() != null) {
throw new Exception("Failed to get discount for flag: " + discountDetails.getFlagKey());
}


String greetingMessage = GreetingMessage.value(client, evalContext);
System.out.println("greetingMessage: " + greetingMessage);
FlagEvaluationDetails<String> greetingDetails = GreetingMessage.valueWithDetails(client, evalContext);
if (greetingDetails.getErrorCode() != null) {
throw new Exception("Error evaluating string flag: " + greetingDetails.getFlagKey());
}
Comment on lines 78 to 80

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Adding the flag key to the error message "Error evaluating string flag" will make it easier to diagnose issues when multiple string flags are being evaluated.

Suggested change
if (greetingDetails.getErrorCode() != null) {
throw new Exception("Error evaluating string flag");
}
if (greetingDetails.getErrorCode() != null) {
throw new Exception("Error evaluating string flag: " + greetingDetails.getFlagKey());
}


Integer usernameMaxLength = UsernameMaxLength.value(client, evalContext);
System.out.println("usernameMaxLength: " + usernameMaxLength);
FlagEvaluationDetails<Integer> usernameDetails = UsernameMaxLength.valueWithDetails(client, evalContext);
if (usernameDetails.getErrorCode() != null) {
throw new Exception("Error evaluating int flag: " + usernameDetails.getFlagKey());
}
Comment on lines 85 to 87

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Including the flag key in the error message "Error evaluating int flag" would provide more specific context for debugging integer flag evaluation failures.

Suggested change
if (usernameDetails.getErrorCode() != null) {
throw new Exception("Error evaluating int flag");
}
if (usernameDetails.getErrorCode() != null) {
throw new Exception("Error evaluating int flag: " + usernameDetails.getFlagKey());
}


Value themeCustomization = ThemeCustomization.value(client, evalContext);
FlagEvaluationDetails<Value> themeDetails = ThemeCustomization.valueWithDetails(client, evalContext);
if (themeDetails.getErrorCode() != null) {
throw new Exception("Error evaluating object flag: " + themeDetails.getFlagKey());
}
Comment on lines 91 to 93

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

For object flags, a more descriptive error message including the flag key would be beneficial for troubleshooting, e.g., "Error evaluating object flag: themeCustomization".

Suggested change
if (themeDetails.getErrorCode() != null) {
throw new Exception("Error evaluating object flag");
}
if (themeDetails.getErrorCode() != null) {
throw new Exception("Error evaluating object flag: " + themeDetails.getFlagKey());
}

System.out.println("themeCustomization: " + themeCustomization);
Comment on lines +61 to +94

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The error checking logic for FlagEvaluationDetails is duplicated for each flag type. This can be refactored into a helper method to reduce code duplication and improve maintainability.

You can add the following helper method to the Main class:

private static <T> void checkDetails(FlagEvaluationDetails<T> details, String flagType) throws Exception {
    if (details.getErrorCode() != null) {
        throw new Exception("Error evaluating " + flagType + " flag: " + details.getFlagKey());
    }
}

Then, you can simplify the evaluation checks as shown in the suggestion.

        Boolean enableFeatureA = EnableFeatureA.value(client, evalContext);
        System.out.println("enableFeatureA: " + enableFeatureA);
        checkDetails(EnableFeatureA.valueWithDetails(client, evalContext), "boolean");

        Double discount = DiscountPercentage.value(client, evalContext);
        System.out.printf("Discount Percentage: %.2f%n", discount);
        checkDetails(DiscountPercentage.valueWithDetails(client, evalContext), "double");

        String greetingMessage = GreetingMessage.value(client, evalContext);
        System.out.println("greetingMessage: " + greetingMessage);
        checkDetails(GreetingMessage.valueWithDetails(client, evalContext), "string");

        Integer usernameMaxLength = UsernameMaxLength.value(client, evalContext);
        System.out.println("usernameMaxLength: " + usernameMaxLength);
        checkDetails(UsernameMaxLength.valueWithDetails(client, evalContext), "int");

        Value themeCustomization = ThemeCustomization.value(client, evalContext);
        checkDetails(ThemeCustomization.valueWithDetails(client, evalContext), "object");
        System.out.println("themeCustomization: " + themeCustomization);


// Test the getKey() method functionality for all flags
System.out.println("enableFeatureA flag key: " + EnableFeatureA.getKey());
System.out.println("discountPercentage flag key: " + DiscountPercentage.getKey());
System.out.println("greetingMessage flag key: " + GreetingMessage.getKey());
System.out.println("usernameMaxLength flag key: " + UsernameMaxLength.getKey());
System.out.println("themeCustomization flag key: " + ThemeCustomization.getKey());
}
}