Skip to content

Commit 2059849

Browse files
committed
Introduces test matrix based on Redis versions [8.0-M1, 7.4.1, 7.2.6, 6.2.16]
Use docker composer to bring up the test env using `redislabs/client-libs-test` image. When run against older Redis version some tests are using commands available only in newer Redis server versions. To resolve this we are introducing two new annotations/rules - Introduce `SinceRedisVersion` annotation/Rule - for conditionally running tests based on Redis server version contacted - Introduce `EnableOnCommad` annotation/Rule - for conditionally running tests based on command availability on the server And mark respective tests with the least Redis Version required by the test - SinceRedisVersion ("7.4.0") - Mark tests using commands/modifiers introduced with Redis 7.4.0 - SinceRedisVersion ("7.2.0") - Mark tests using commands/modifiers introduced with Redis 7.2.0 - SinceRedisVersion ("7.0.0") - Mark tests using commands/modifiers introduced with Redis 7.0.0 The same approach used to mark CSC tests - Disabled client-side caching tests for versions below 7.4 Fix in Jedis Client against Redis server 6.x - Fix NPW on CommandInfo - some of the array elements returned are available based from given RedisServer aclCategories (as of Redis 6.0) , tips , (as of Redis 7.0) subcommands - Fix NPE AccessControlLogEntry when used against Redis 6 Starting with Redis version 7.2.0: Added entry ID, timestamp created, and timestamp last updated fields. Fix Test failures against 6.x - Fix JedisPooledClientSideCacheTest - Fix AccessControlListCommandsTest.aclLogTest:372 » NullPointer - Fix AccessControlListCommandsTest.aclLogWithEntryID:473 » NullPointer - Fix StreamsCommandsTest - Fix StreamsPipelineCommandsTest xadd - Starting with Redis version 7.0.0: Added support for the <ms>-* explicit ID form. - Test env migrated to use native Redis server TLS instead of using stunnel Not all tests were migrated - Disable Modules test in containerized test env ModuleTest uses custom test module to test load/unload/sendCommand. Requires pre-build test module on the same os like test container to avoid errors - Disable UDS tests in containerized test env No easy way to make unix sockets work on MAC with docker
1 parent 9b88636 commit 2059849

File tree

127 files changed

+2532
-619
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

127 files changed

+2532
-619
lines changed

.github/workflows/integration.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ jobs:
3434
- name: System setup
3535
run: |
3636
sudo apt update
37-
sudo apt install -y stunnel make
38-
make system-setup
37+
sudo apt install -y make
38+
make compile-module
3939
- name: Cache dependencies
4040
uses: actions/cache@v2
4141
with:
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
---
2+
3+
name: Build and Test using containerized environment
4+
5+
on:
6+
push:
7+
paths-ignore:
8+
- 'docs/**'
9+
- '**/*.md'
10+
- '**/*.rst'
11+
branches:
12+
- master
13+
- '[0-9].*'
14+
pull_request:
15+
branches:
16+
- master
17+
- '[0-9].*'
18+
schedule:
19+
- cron: '0 1 * * *' # nightly build
20+
workflow_dispatch:
21+
inputs:
22+
specific_test:
23+
description: 'Run specific test(s) (optional)'
24+
required: false
25+
default: ''
26+
jobs:
27+
28+
build:
29+
name: Build and Test
30+
runs-on: ubuntu-latest
31+
env:
32+
REDIS_ENV_WORK_DIR: ${{ github.workspace }}/redis-env-work
33+
REDIS_ENV_CONF_DIR: ${{ github.workspace }}/src/test/resources/env
34+
CLIENT_LIBS_IMAGE_PREFIX: "redislabs/client-libs-test"
35+
strategy:
36+
fail-fast: false
37+
matrix:
38+
redis_version:
39+
- "8.0-M01"
40+
- "7.4.1"
41+
- "7.2.6"
42+
- "6.2.16"
43+
steps:
44+
- uses: actions/checkout@v2
45+
- name: Set up publishing to maven central
46+
uses: actions/setup-java@v2
47+
with:
48+
java-version: '8'
49+
distribution: 'temurin'
50+
- name: System setup
51+
run: |
52+
sudo apt update
53+
sudo apt install -y make
54+
make compile-module
55+
- name: Cache dependencies
56+
uses: actions/cache@v2
57+
with:
58+
path: |
59+
~/.m2/repository
60+
/var/cache/apt
61+
key: jedis-${{hashFiles('**/pom.xml')}}
62+
# Set up Docker Compose environment
63+
- name: Set up Docker Compose environment
64+
run: |
65+
mkdir -m 777 $REDIS_ENV_WORK_DIR
66+
export CLIENT_LIBS_TEST_IMAGE="${CLIENT_LIBS_IMAGE_PREFIX}:${{ matrix.redis_version }}"
67+
export COMPOSE_ENV_FILES="src/test/resources/env/.env"
68+
if [[ "${{ matrix.redis_version }}" == "6.2.16" ]]; then
69+
COMPOSE_ENV_FILES+=",src/test/resources/env/.env.v${{ matrix.redis_version }}"
70+
fi
71+
docker compose -f src/test/resources/env/docker-compose.yml up -d
72+
- name: Maven offline
73+
run: |
74+
mvn -q dependency:go-offline
75+
- name: Build docs
76+
run: |
77+
mvn javadoc:jar
78+
# Run Tests
79+
- name: Run Maven tests
80+
run: |
81+
export TEST_ENV_PROVIDER=docker
82+
export TEST_WORK_FOLDER=$REDIS_ENV_WORK_DIR
83+
echo $TEST_WORK_FOLDER
84+
if [ -z "$TESTS" ]; then
85+
mvn clean compile test
86+
else
87+
mvn -Dtest=$SPECIFIC_TEST clean compile test
88+
fi
89+
env:
90+
TESTS: ${{ github.event.inputs.specific_test || '' }}
91+
- name: Publish Test Results
92+
uses: EnricoMi/publish-unit-test-result-action@v2
93+
if: always()
94+
with:
95+
files: |
96+
target/surefire-reports/**/*.xml
97+
# Collect logs on failure
98+
- name: Collect logs on failure
99+
if: failure() # This runs only if the previous steps failed
100+
run: |
101+
echo "Collecting logs from $WORK_DIR..."
102+
ls -la $REDIS_ENV_WORK_DIR
103+
# Upload logs as artifacts
104+
- name: Upload logs on failure
105+
if: failure()
106+
uses: actions/upload-artifact@v3
107+
with:
108+
name: redis-env-work-logs
109+
path: ${{ env.REDIS_ENV_WORK_DIR }}
110+
# Bring down the Docker Compose test environment
111+
- name: Tear down Docker Compose environment
112+
if: always()
113+
run: |
114+
docker compose $COMPOSE_ENV_FILES -f src/test/resources/env/docker-compose.yml down
115+
continue-on-error: true
116+
# Upload code coverage
117+
- name: Upload coverage to Codecov
118+
uses: codecov/codecov-action@v4
119+
with:
120+
fail_ci_if_error: false
121+
token: ${{ secrets.CODECOV_TOKEN }}
122+
- name: Upload test results to Codecov
123+
if: ${{ github.event_name == 'schedule' || (github.event_name == 'push') || github.event_name == 'workflow_dispatch'}}
124+
uses: codecov/test-results-action@v1
125+
with:
126+
fail_ci_if_error: false
127+
files: ./target/surefire-reports/TEST*
128+
token: ${{ secrets.CODECOV_TOKEN }}

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ build/
1111
bin/
1212
tags
1313
.idea
14+
.run
1415
*.aof
1516
*.rdb
1617
redis-git
1718
appendonlydir/
19+
.DS_Store

Makefile

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ protected-mode no
77
port 6379
88
requirepass foobared
99
user acljedis on allcommands allkeys >fizzbuzz
10+
user deploy on allcommands allkeys >verify
1011
pidfile /tmp/redis1.pid
1112
logfile /tmp/redis1.log
1213
save ""
@@ -189,6 +190,7 @@ endef
189190

190191
define REDIS_SENTINEL5
191192
port 26383
193+
tlsport 36383
192194
daemonize yes
193195
protected-mode no
194196
user default off
@@ -525,8 +527,14 @@ mvn-release:
525527
mvn release:prepare
526528
mvn release:perform -DskipTests
527529

528-
system-setup:
529-
sudo apt install -y gcc g++
530+
install-gcc:
531+
@if [ "$(shell uname)" = "Darwin" ]; then \
532+
brew install gcc; \
533+
else \
534+
sudo apt install -y gcc g++; \
535+
fi
536+
537+
system-setup: install-gcc
530538
[ ! -e redis-git ] && git clone https://github.com/redis/redis.git --branch unstable --single-branch redis-git || true
531539
$(MAKE) -C redis-git clean
532540
$(MAKE) -C redis-git

src/main/java/redis/clients/jedis/resps/AccessControlLogEntry.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ public class AccessControlLogEntry implements Serializable {
3838
private final long timestampCreated;
3939
private final long timestampLastUpdated;
4040

41+
/*
42+
* Starting with Redis version 7.2.0: Added entry ID, timestamp created, and timestamp last updated.
43+
* @see https://redis.io/docs/latest/commands/acl-log/
44+
*/
4145
public AccessControlLogEntry(Map<String, Object> map) {
4246
count = (long) map.get(COUNT);
4347
reason = (String) map.get(REASON);
@@ -47,9 +51,9 @@ public AccessControlLogEntry(Map<String, Object> map) {
4751
ageSeconds = (Double) map.get(AGE_SECONDS);
4852
clientInfo = getMapFromRawClientInfo((String) map.get(CLIENT_INFO));
4953
logEntry = map;
50-
entryId = (long) map.get(ENTRY_ID);
51-
timestampCreated = (long) map.get(TIMESTAMP_CREATED);
52-
timestampLastUpdated = (long) map.get(TIMESTAMP_LAST_UPDATED);
54+
entryId = map.get(ENTRY_ID) == null ? 0L : (long) map.get(ENTRY_ID);
55+
timestampCreated = map.get(TIMESTAMP_CREATED) == null ? 0L : (long) map.get(TIMESTAMP_CREATED);
56+
timestampLastUpdated = map.get(TIMESTAMP_LAST_UPDATED) == null ? 0L : (long) map.get(TIMESTAMP_LAST_UPDATED);
5357
}
5458

5559
public long getCount() {

src/main/java/redis/clients/jedis/resps/CommandInfo.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import redis.clients.jedis.Builder;
44

5+
import java.util.Collections;
56
import java.util.List;
67

78
import static redis.clients.jedis.BuilderFactory.STRING_LIST;
@@ -103,9 +104,13 @@ public CommandInfo build(Object data) {
103104
long firstKey = LONG.build(commandData.get(3));
104105
long lastKey = LONG.build(commandData.get(4));
105106
long step = LONG.build(commandData.get(5));
106-
List<String> aclCategories = STRING_LIST.build(commandData.get(6));
107-
List<String> tips = STRING_LIST.build(commandData.get(7));
108-
List<String> subcommands = STRING_LIST.build(commandData.get(9));
107+
108+
// (as of Redis 6.0)
109+
List<String> aclCategories = commandData.size()>=6?STRING_LIST.build(commandData.get(6)):Collections.emptyList();
110+
111+
// (as of Redis 7.0)
112+
List<String> tips = commandData.size()>=8?STRING_LIST.build(commandData.get(7)):Collections.emptyList();
113+
List<String> subcommands = commandData.size()>=10?STRING_LIST.build(commandData.get(9)): Collections.emptyList();
109114

110115
return new CommandInfo(arity, flags, firstKey, lastKey, step, aclCategories, tips, subcommands);
111116
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package io.redis.test.annotations;
2+
3+
import java.lang.annotation.*;
4+
5+
@Inherited
6+
@Retention(RetentionPolicy.RUNTIME)
7+
@Target({ElementType.METHOD, ElementType.TYPE})
8+
public @interface EnabledOnCommand {
9+
String value();
10+
String subCommand() default "";
11+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package io.redis.test.annotations;
2+
3+
import java.lang.annotation.*;
4+
5+
@Inherited
6+
@Retention(RetentionPolicy.RUNTIME)
7+
@Target({ElementType.METHOD, ElementType.TYPE})
8+
public @interface SinceRedisVersion {
9+
String value();
10+
String message() default "";
11+
}
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package io.redis.test.utils;
2+
3+
import io.redis.test.annotations.EnabledOnCommand;
4+
import org.junit.Assume;
5+
import org.junit.rules.TestRule;
6+
import org.junit.runner.Description;
7+
import org.junit.runners.model.Statement;
8+
import org.slf4j.Logger;
9+
import org.slf4j.LoggerFactory;
10+
import redis.clients.jedis.*;
11+
import redis.clients.jedis.resps.CommandInfo;
12+
13+
import java.lang.reflect.Method;
14+
import java.util.Map;
15+
16+
17+
public class EnabledOnCommandRule implements TestRule {
18+
private static final Logger logger = LoggerFactory.getLogger(EnabledOnCommandRule.class);
19+
20+
private final HostAndPort hostPort;
21+
private final JedisClientConfig config;
22+
23+
public EnabledOnCommandRule(HostAndPort hostPort, JedisClientConfig config) {
24+
this.hostPort = hostPort;
25+
this.config = config;
26+
}
27+
28+
public EnabledOnCommandRule(EndpointConfig endpointConfig) {
29+
this.hostPort = endpointConfig.getHostAndPort();
30+
this.config = endpointConfig.getClientConfigBuilder().build();
31+
}
32+
33+
@Override
34+
public Statement apply(Statement base, Description description) {
35+
return new Statement() {
36+
@Override
37+
public void evaluate() throws Throwable {
38+
try (Jedis jedisClient = new Jedis(hostPort, config)) {
39+
String[] command = getCommandFromAnnotations(description);
40+
41+
if (command != null && !isCommandAvailable(jedisClient, command[0],command[1])) {
42+
Assume.assumeTrue("Test requires Redis command '" + command[0] + " " + command[1] + "' to be available, but it was not found.", false);
43+
}
44+
45+
base.evaluate();
46+
}
47+
}
48+
49+
/**
50+
* Retrieves the command from either class-level or method-level annotations.
51+
*
52+
* @param description The test description containing annotations.
53+
* @return The Redis array containing command, subcommand from the annotations, or null if not found.
54+
*/
55+
private String[] getCommandFromAnnotations(Description description) {
56+
// Retrieve class-level and method-level annotations
57+
EnabledOnCommand descriptionCommandAnnotation = description.getAnnotation(EnabledOnCommand.class);
58+
if (descriptionCommandAnnotation != null) {
59+
return new String[] {descriptionCommandAnnotation.value(), descriptionCommandAnnotation.subCommand()};
60+
}
61+
62+
EnabledOnCommand methodCommand = getMethodAnnotation(description);
63+
if (methodCommand != null) {
64+
return new String[] {methodCommand.value(), methodCommand.subCommand()};
65+
}
66+
67+
EnabledOnCommand classCommand = description.getTestClass().getAnnotation(EnabledOnCommand.class);
68+
if (classCommand != null) {
69+
return new String[] {classCommand.value(), classCommand.subCommand()};
70+
}
71+
72+
return null;
73+
}
74+
75+
private EnabledOnCommand getMethodAnnotation(Description description) {
76+
try {
77+
// description.getAnnotation() does not return anootaion when used
78+
// with parametrised tests
79+
String methodName = description.getMethodName();
80+
if (methodName != null) {
81+
Class<?> testClass = description.getTestClass();
82+
if (testClass != null) {
83+
for (Method method : testClass.getDeclaredMethods()) {
84+
if (method.getName().equals(methodName)) {
85+
return method.getAnnotation(EnabledOnCommand.class);
86+
}
87+
}
88+
}
89+
}
90+
} catch (Exception e) {
91+
// Handle any potential exceptions here
92+
throw new RuntimeException("Could not resolve EnabledOnCommand annotation",e);
93+
}
94+
return null;
95+
}
96+
97+
/**
98+
* Checks if the specified Redis command is available.
99+
*/
100+
private boolean isCommandAvailable(Jedis jedisClient, String command, String subCommand) {
101+
try {
102+
Object raw = jedisClient.sendCommand(redis.clients.jedis.Protocol.Command.COMMAND);
103+
Map<String, CommandInfo> commandList = BuilderFactory.COMMAND_INFO_RESPONSE.build(raw);
104+
CommandInfo commandInfo = commandList.get(command.toLowerCase());
105+
if (commandInfo != null) {
106+
// If a subCommand is provided, check for the subcommand under this command
107+
if (subCommand != null && !subCommand.isEmpty()) {
108+
// Check if this command supports the provided subcommand
109+
for (String supportedSubCommand : commandInfo.getSubcommands()) {
110+
if (subCommand.equalsIgnoreCase(supportedSubCommand)) {
111+
return true;
112+
}
113+
}
114+
return false; // Subcommand not found
115+
}
116+
return true; // Command found (no subcommand required)
117+
}
118+
return false; // Command not found
119+
} catch (Exception e) {
120+
logger.error("Error checking command '{}' availability: {}", command, e.getMessage());
121+
return false;
122+
}
123+
}
124+
};
125+
}
126+
}

0 commit comments

Comments
 (0)