Skip to content

Commit 5714928

Browse files
dkasimovskiyclaude
andauthored
[testcontainers] Support both TQE 2.x and TQE 3.x versions (#86)
* feat(testcontainers): add TQE 3.x support and unify with 2.x via parameterized tests Add TQE 3.x (message-queue-ee 3.x) integration on top of the existing TQE 2.x support in testcontainers. Refactor the resulting duplication into a single parameterized test suite driven by a per-version enum, versioned cluster and gRPC strategy implementations, and shared fixtures. - New TQE 3.x wiring: image, configs, ProducerServiceGrpc client, gRPC container, bidirectional subscription. - Versioned cluster/container builders via FileTQEConfigurator.tqe3Builder /tqe2Builder; single TQEClusterImpl (TQE 3.x startup order is safe for both versions). - GrpcRole.PRODUCER accepts "publisher" as alias (TQE 2.x naming) — eliminates the version-specific role constant. - Replace TQE2ClusterImplTest + TQE3ClusterImplTest with TQEClusterIntegrationTest + TQEClusterTest parameterized over TQEVersion. - Extract GrpcTestStrategy interface with TQE2/TQE3 implementations to isolate version-specific gRPC API differences. - TQEVersion encapsulates image, configs, role names, builder/cluster factories, and gRPC strategy per version (OCP-friendly — adding a future TQE 4.x is a single new enum constant). - Migrate protobuf generation from xolstice to ascopes plugin; rename proto/config resource directories to tqe2/tqe3. - Fix GrpcContainerImpl.validateConfigPath: path.toString().endsWith() instead of Path.endsWith (path-component comparison) so the .yml extension check actually works as the error message claims. Addresses PR #86 review comments. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * refactor(testcontainers): address PR #86 review feedback - FileTQEConfigurator: pass routerRole through private Builder constructor instead of exposing withRouterRole() setter; remove dead null check in build() - Rename GrpcTestStrategy -> TQEClient and implementations -> TQE2Client/TQE3Client - Replace Set<User> with Consumer<User> in subscribe() for flexible result handling - TQEVersion: drop unused displayName()/requiresConfigure() and trivial createCluster() factory (TQEClusterImpl is created directly in tests) - FileTQEConfiguratorTest: always call configure() (idempotent for both versions) - TQEClusterIntegrationTest: simplify size check to users.size() == result.size() * refactor(testcontainers): hide ManagedChannel from TQEClient API Per PR #86 review (r3381299111): ManagedChannel is an internal transport detail and should not be part of the TQEClient contract. - TQEClient.publish/subscribe: drop ManagedChannel parameter - TQE2Client/TQE3Client: take the channel in the constructor and store it as a field; remove INSTANCE singletons - TQEVersion: replace stored TQEClient field with an abstract client(ManagedChannel) factory, overridden per version - TQEClusterFixture: own the channels and the bound clients; expose publisherClient()/consumerClient() and shut down channels in close() - TQEClusterIntegrationTest: drop manual channel creation/shutdown and the now-unused ManagedChannel import --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
1 parent f813243 commit 5714928

28 files changed

Lines changed: 1525 additions & 589 deletions

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88

99
### Testcontainers
1010

11+
- Add TQE 3.x (message-queue-ee 3.x) integration on top of the
12+
existing TQE 2.x support; consolidate the TQE 2.x / 3.x test
13+
surface into a single parameterized suite.
1114
- Add constructor/builder parameters to supply the initial Lua script as a string or as a file path, and optional additional script paths copied into the container data directory (`Tarantool2Container`, `CartridgeClusterContainer`, `VshardClusterContainer`); simplify bundled `server.lua` accordingly.
1215
- Upgrade TQE to v3.5.0.
1316

testcontainers/pom.xml

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
<properties>
1919
<license.header.file>${project.parent.basedir}/LICENSE_HEADER.txt</license.header.file>
2020
<maven.compiler.release>17</maven.compiler.release>
21-
<protobuf-plugin.version>0.6.1</protobuf-plugin.version>
21+
<protobuf-plugin.version>5.1.4</protobuf-plugin.version>
2222
</properties>
2323

2424
<dependencyManagement>
@@ -104,13 +104,6 @@
104104
</dependencies>
105105

106106
<build>
107-
<extensions>
108-
<extension>
109-
<groupId>kr.motd.maven</groupId>
110-
<artifactId>os-maven-plugin</artifactId>
111-
<version>1.7.0</version>
112-
</extension>
113-
</extensions>
114107
<plugins>
115108
<plugin>
116109
<groupId>org.jsonschema2pojo</groupId>
@@ -146,25 +139,45 @@
146139
</dependencies>
147140
</plugin>
148141
<plugin>
149-
<groupId>org.xolstice.maven.plugins</groupId>
142+
<groupId>io.github.ascopes</groupId>
150143
<artifactId>protobuf-maven-plugin</artifactId>
151144
<version>${protobuf-plugin.version}</version>
152145
<configuration>
153-
<protoTestSourceRoot>${project.basedir}/src/test/proto</protoTestSourceRoot>
154-
<protocArtifact>
155-
com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}
156-
</protocArtifact>
157-
<pluginId>grpc-java</pluginId>
158-
<pluginArtifact>
159-
io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}
160-
</pluginArtifact>
146+
<protoc kind="binary-maven">
147+
<version>${protobuf.version}</version>
148+
</protoc>
149+
<plugins>
150+
<plugin kind="binary-maven">
151+
<groupId>io.grpc</groupId>
152+
<artifactId>protoc-gen-grpc-java</artifactId>
153+
<version>${grpc.version}</version>
154+
</plugin>
155+
</plugins>
161156
</configuration>
162157
<executions>
163158
<execution>
159+
<id>generate-test-tqe3</id>
164160
<goals>
165-
<goal>test-compile</goal>
166-
<goal>test-compile-custom</goal>
161+
<goal>generate-test</goal>
167162
</goals>
163+
<configuration>
164+
<sourceDirectories>
165+
<sourceDirectory>${project.basedir}/src/test/proto/tqe3</sourceDirectory>
166+
</sourceDirectories>
167+
<outputDirectory>${project.build.directory}/generated-test-sources/protobuf/tqe3</outputDirectory>
168+
</configuration>
169+
</execution>
170+
<execution>
171+
<id>generate-test-tqe2</id>
172+
<goals>
173+
<goal>generate-test</goal>
174+
</goals>
175+
<configuration>
176+
<sourceDirectories>
177+
<sourceDirectory>${project.basedir}/src/test/proto/tqe2</sourceDirectory>
178+
</sourceDirectories>
179+
<outputDirectory>${project.build.directory}/generated-test-sources/protobuf/tqe2</outputDirectory>
180+
</configuration>
168181
</execution>
169182
</executions>
170183
</plugin>

testcontainers/src/main/java/org/testcontainers/containers/tqe/GrpcContainer.java

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import java.nio.file.Path;
1010
import java.nio.file.Paths;
1111
import java.util.HashMap;
12+
import java.util.List;
1213
import java.util.Map;
1314
import java.util.Set;
1415

@@ -50,24 +51,26 @@ public interface GrpcContainer<SELF extends GrpcContainer<SELF>>
5051
enum GrpcRole {
5152
CONSUMER("consumer"),
5253

53-
PRODUCER("producer");
54+
PRODUCER("producer", "publisher");
5455

55-
private final String role;
56+
private final List<String> aliases;
5657

5758
private static final Map<String, GrpcRole> ROLES = new HashMap<>();
5859

5960
static {
6061
for (GrpcRole value : values()) {
61-
ROLES.put(value.getRole(), value);
62+
for (String alias : value.aliases) {
63+
ROLES.put(alias, value);
64+
}
6265
}
6366
}
6467

65-
GrpcRole(String role) {
66-
this.role = role;
68+
GrpcRole(String... aliases) {
69+
this.aliases = List.of(aliases);
6770
}
6871

6972
public String getRole() {
70-
return this.role;
73+
return this.aliases.get(0);
7174
}
7275

7376
public static GrpcRole from(String role) {

testcontainers/src/main/java/org/testcontainers/containers/tqe/GrpcContainerImpl.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ private static void validateConfigPath(Path configPath) {
163163
if (configPath == null
164164
|| !Files.exists(configPath)
165165
|| !Files.isRegularFile(configPath)
166-
|| configPath.endsWith(".yml")) {
166+
|| !configPath.toString().endsWith(".yml")) {
167167
LOGGER.error(
168168
"Invalid config file. Config path is null or not exists or not regular or not having"
169169
+ " '.yml' extension: {}",
@@ -223,11 +223,11 @@ private static Set<Integer> resolveGrpcPorts(GrpcConfiguration config, Path conf
223223
* (required).
224224
*/
225225
private static Set<GrpcRole> resolveRoles(GrpcConfiguration config, Path configPath) {
226-
final Optional<Boolean> isPublisher = config.getProducer().flatMap(ProducerConfig::getEnabled);
226+
final Optional<Boolean> isProducer = config.getProducer().flatMap(ProducerConfig::getEnabled);
227227
final Set<GrpcRole> roles = new LinkedHashSet<>();
228-
if (isPublisher.isPresent() && isPublisher.get()) {
228+
if (isProducer.isPresent() && isProducer.get()) {
229229
roles.add(GrpcRole.PRODUCER);
230-
LOGGER.trace("Publisher role is enabled for: {}", configPath);
230+
LOGGER.trace("Producer role is enabled for: {}", configPath);
231231
}
232232

233233
final Optional<Boolean> isConsumer = config.getConsumer().flatMap(ConsumerConfig::getEnabled);

testcontainers/src/main/java/org/testcontainers/containers/tqe/configuration/FileTQEConfigurator.java

Lines changed: 51 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@
4747
import io.tarantool.autogen.iproto.listen.Listen;
4848

4949
/**
50-
* Base implementation of {@link TQEConfigurator} that configure TQE cluster using configuration
51-
* files. All other implementations should extend this class.
50+
* Configures TQE cluster using configuration files. Supports both TQE 2.x and TQE 3.x via factory
51+
* methods {@link #tqe2Builder} and {@link #tqe3Builder}.
5252
*/
5353
public class FileTQEConfigurator implements TQEConfigurator {
5454

@@ -64,11 +64,12 @@ public class FileTQEConfigurator implements TQEConfigurator {
6464
/* Constants
6565
/**********************************************************
6666
*/
67+
private static final String TQE2_ROUTER_ROLE = "app.roles.api";
68+
private static final String TQE3_ROUTER_ROLE = "roles.tqe-router";
69+
6770
private static final String CONFIGURATOR_ERROR_MSG =
6871
"An error occurred when configuring the TQE cluster. See logs for details.";
6972

70-
private static final String TQE_ROUTER_ROLE = "roles.tqe-router";
71-
7273
/*
7374
/**********************************************************
7475
/* Parameter extractors
@@ -97,6 +98,8 @@ public class FileTQEConfigurator implements TQEConfigurator {
9798

9899
private final Network network;
99100

101+
private final String routerRole;
102+
100103
private boolean configured;
101104

102105
private FileTQEConfigurator(
@@ -105,11 +108,13 @@ private FileTQEConfigurator(
105108
Collection<Path> grpcConfigs,
106109
String clusterName,
107110
Duration startupTimeout,
108-
Duration bootstrapTimeout) {
111+
Duration bootstrapTimeout,
112+
String routerRole) {
109113
this.queueConfig = queueConfig;
110114
this.grpcConfigs = grpcConfigs;
111115
this.clusterName = clusterName;
112116
this.bootstrapTimeout = bootstrapTimeout;
117+
this.routerRole = routerRole;
113118
this.routerNames = new LinkedHashSet<>(1);
114119
this.image = image;
115120
this.startupTimeout = startupTimeout;
@@ -156,9 +161,9 @@ private Map<String, TarantoolContainer<?>> initQueue(
156161
final Map<String, TarantoolContainer<?>> nodes = new LinkedHashMap<>(instances.size());
157162

158163
this.routerNames.addAll(
159-
ConfigurationUtils.findInstancesWithRole(this.queueParsedConfig, TQE_ROUTER_ROLE));
164+
ConfigurationUtils.findInstancesWithRole(this.queueParsedConfig, this.routerRole));
160165
if (this.routerNames.isEmpty()) {
161-
LOGGER.error("At least one container must have the 'router' and '{}' roles", TQE_ROUTER_ROLE);
166+
LOGGER.error("At least one container must have the 'router' and '{}' roles", this.routerRole);
162167
throw new ContainerLaunchException(CONFIGURATOR_ERROR_MSG);
163168
}
164169

@@ -436,11 +441,41 @@ public synchronized void close() {
436441
}
437442
}
438443

439-
public static Builder builder(
444+
/**
445+
* Creates a builder pre-configured for TQE 2.x (router role: {@value #TQE2_ROUTER_ROLE}).
446+
*
447+
* @param image Docker image name
448+
* @param queueConfig path to queue configuration file
449+
* @param grpcConfigs paths to gRPC configuration files
450+
* @return builder with TQE 2.x router role pre-set
451+
*/
452+
public static Builder tqe2Builder(
440453
DockerImageName image, Path queueConfig, Collection<Path> grpcConfigs) {
441-
return new Builder(image, queueConfig, grpcConfigs);
454+
return builder(image, queueConfig, grpcConfigs, TQE2_ROUTER_ROLE);
442455
}
443456

457+
/**
458+
* Creates a builder pre-configured for TQE 3.x (router role: {@value #TQE3_ROUTER_ROLE}).
459+
*
460+
* @param image Docker image name
461+
* @param queueConfig path to queue configuration file
462+
* @param grpcConfigs paths to gRPC configuration files
463+
* @return builder with TQE 3.x router role pre-set
464+
*/
465+
public static Builder tqe3Builder(
466+
DockerImageName image, Path queueConfig, Collection<Path> grpcConfigs) {
467+
return builder(image, queueConfig, grpcConfigs, TQE3_ROUTER_ROLE);
468+
}
469+
470+
private static Builder builder(
471+
DockerImageName image, Path queueConfig, Collection<Path> grpcConfigs, String routerRole) {
472+
return new Builder(image, queueConfig, grpcConfigs, routerRole);
473+
}
474+
475+
/**
476+
* Builder for {@link FileTQEConfigurator}. Use factory methods {@link #tqe2Builder} or {@link
477+
* #tqe3Builder} to obtain a pre-configured builder.
478+
*/
444479
public static class Builder {
445480

446481
private static final String DEFAULT_CLUSTER_NAME_PREFIX = "tqe-test";
@@ -450,8 +485,10 @@ public static class Builder {
450485
private String clusterName;
451486
private Duration startupTimeout;
452487
private Duration bootstrapTimeout;
488+
private final String routerRole;
453489

454-
public Builder(DockerImageName image, Path queueConfig, Collection<Path> grpcConfigs) {
490+
private Builder(
491+
DockerImageName image, Path queueConfig, Collection<Path> grpcConfigs, String routerRole) {
455492
if (queueConfig == null || !Files.isRegularFile(queueConfig)) {
456493
LOGGER.error("Queue config file is invalid (null or not regular): {})", queueConfig);
457494
throw new IllegalArgumentException(CONFIGURATOR_ERROR_MSG);
@@ -469,9 +506,11 @@ public Builder(DockerImageName image, Path queueConfig, Collection<Path> grpcCon
469506
throw new IllegalArgumentException(CONFIGURATOR_ERROR_MSG);
470507
}
471508
});
509+
472510
this.image = image;
473511
this.queueConfig = queueConfig;
474512
this.grpcConfigs = new LinkedHashSet<>(grpcConfigs);
513+
this.routerRole = routerRole;
475514
}
476515

477516
public Builder withClusterName(String clusterName) {
@@ -514,7 +553,8 @@ public FileTQEConfigurator build() {
514553
this.grpcConfigs,
515554
clusterName,
516555
startupTimeout,
517-
bootstrapTimeout);
556+
bootstrapTimeout,
557+
this.routerRole);
518558
}
519559
}
520560
}

testcontainers/src/main/java/org/testcontainers/containers/tqe/configuration/grpc/GrpcConfiguration.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.util.Optional;
99
import java.util.Set;
1010

11+
import com.fasterxml.jackson.annotation.JsonAlias;
1112
import com.fasterxml.jackson.annotation.JsonCreator;
1213
import com.fasterxml.jackson.annotation.JsonProperty;
1314

@@ -73,6 +74,7 @@ public class GrpcConfiguration {
7374
private final Boolean daemon;
7475

7576
@JsonProperty("producer")
77+
@JsonAlias("publisher")
7678
private final ProducerConfig producer;
7779

7880
@JsonProperty("consumer")

testcontainers/src/test/java/org/testcontainers/containers/integration/tqe/CommonTest.java

Lines changed: 0 additions & 48 deletions
This file was deleted.

0 commit comments

Comments
 (0)