mirror of
https://github.com/oven-sh/bun
synced 2026-02-09 10:28:47 +00:00
449 lines
16 KiB
TypeScript
449 lines
16 KiB
TypeScript
import { beforeEach, describe, expect, test } from "bun:test";
|
|
import { isCI } from "harness";
|
|
import { ConnectionType, createClient, ctx, expectType, isEnabled } from "../test-utils";
|
|
|
|
/**
|
|
* Test suite covering Redis list operations
|
|
* - Basic operations (LPUSH, RPUSH, LPOP, RPOP)
|
|
* - Range operations (LRANGE, LTRIM)
|
|
* - List information (LLEN, LINDEX)
|
|
* - Blocking operations (BLPOP, BRPOP)
|
|
*/
|
|
describe.skipIf(!isEnabled)("Valkey: List Data Type Operations", () => {
|
|
beforeEach(() => {
|
|
if (ctx.redis?.connected) {
|
|
ctx.redis.close?.();
|
|
}
|
|
ctx.redis = createClient(ConnectionType.TCP);
|
|
});
|
|
|
|
describe("Basic List Operations", () => {
|
|
test("LPUSH and RPUSH commands", async () => {
|
|
const key = ctx.generateKey("list-push-test");
|
|
|
|
// Left push single value
|
|
const lpushResult = await ctx.redis.send("LPUSH", [key, "left-value"]);
|
|
expectType<number>(lpushResult, "number");
|
|
expect(lpushResult).toBe(1); // List has 1 element
|
|
|
|
// Right push single value
|
|
const rpushResult = await ctx.redis.send("RPUSH", [key, "right-value"]);
|
|
expectType<number>(rpushResult, "number");
|
|
expect(rpushResult).toBe(2); // List now has 2 elements
|
|
|
|
// Multiple values with LPUSH
|
|
const multiLpushResult = await ctx.redis.send("LPUSH", [key, "left1", "left2", "left3"]);
|
|
expectType<number>(multiLpushResult, "number");
|
|
expect(multiLpushResult).toBe(5); // List now has 5 elements
|
|
|
|
// Multiple values with RPUSH
|
|
const multiRpushResult = await ctx.redis.send("RPUSH", [key, "right1", "right2"]);
|
|
expectType<number>(multiRpushResult, "number");
|
|
expect(multiRpushResult).toBe(7); // List now has 7 elements
|
|
|
|
// Verify the list content (should be left3, left2, left1, left-value, right-value, right1, right2)
|
|
const range = await ctx.redis.send("LRANGE", [key, "0", "-1"]);
|
|
expect(Array.isArray(range)).toBe(true);
|
|
expect(range.length).toBe(7);
|
|
expect(range[0]).toBe("left3");
|
|
expect(range[3]).toBe("left-value");
|
|
expect(range[4]).toBe("right-value");
|
|
expect(range[6]).toBe("right2");
|
|
});
|
|
|
|
test("LPOP and RPOP commands", async () => {
|
|
const key = ctx.generateKey("list-pop-test");
|
|
|
|
// Set up test list
|
|
await ctx.redis.send("RPUSH", [key, "one", "two", "three", "four", "five"]);
|
|
|
|
// Pop from left side
|
|
const lpopResult = await ctx.redis.send("LPOP", [key]);
|
|
expect(lpopResult).toBe("one");
|
|
|
|
// Pop from right side
|
|
const rpopResult = await ctx.redis.send("RPOP", [key]);
|
|
expect(rpopResult).toBe("five");
|
|
|
|
// Pop multiple elements from left
|
|
const multiLpopResult = await ctx.redis.send("LPOP", [key, "2"]);
|
|
expect(Array.isArray(multiLpopResult)).toBe(true);
|
|
expect(multiLpopResult.length).toBe(2);
|
|
expect(multiLpopResult[0]).toBe("two");
|
|
expect(multiLpopResult[1]).toBe("three");
|
|
|
|
// Verify only "four" is left
|
|
const remaining = await ctx.redis.send("LRANGE", [key, "0", "-1"]);
|
|
expect(Array.isArray(remaining)).toBe(true);
|
|
expect(remaining.length).toBe(1);
|
|
expect(remaining[0]).toBe("four");
|
|
});
|
|
|
|
test("LRANGE command", async () => {
|
|
const key = ctx.generateKey("lrange-test");
|
|
|
|
// Set up test list with 10 elements
|
|
await ctx.redis.send("RPUSH", [key, "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]);
|
|
|
|
// Get full range - using LRANGE command
|
|
// TODO: When a direct lrange method is implemented, use that instead
|
|
const fullRange = await ctx.redis.send("LRANGE", [key, "0", "-1"]);
|
|
expect(Array.isArray(fullRange)).toBe(true);
|
|
expect(fullRange).toMatchInlineSnapshot(`
|
|
[
|
|
"0",
|
|
"1",
|
|
"2",
|
|
"3",
|
|
"4",
|
|
"5",
|
|
"6",
|
|
"7",
|
|
"8",
|
|
"9",
|
|
]
|
|
`);
|
|
|
|
// Get partial range from start
|
|
const startRange = await ctx.redis.send("LRANGE", [key, "0", "2"]);
|
|
expect(Array.isArray(startRange)).toBe(true);
|
|
expect(startRange).toMatchInlineSnapshot(`
|
|
[
|
|
"0",
|
|
"1",
|
|
"2",
|
|
]
|
|
`);
|
|
|
|
// Get partial range from middle
|
|
const midRange = await ctx.redis.send("LRANGE", [key, "3", "6"]);
|
|
expect(Array.isArray(midRange)).toBe(true);
|
|
expect(midRange).toMatchInlineSnapshot(`
|
|
[
|
|
"3",
|
|
"4",
|
|
"5",
|
|
"6",
|
|
]
|
|
`);
|
|
|
|
// Get partial range from end using negative indices
|
|
const endRange = await ctx.redis.send("LRANGE", [key, "-3", "-1"]);
|
|
expect(Array.isArray(endRange)).toBe(true);
|
|
expect(endRange).toMatchInlineSnapshot(`
|
|
[
|
|
"7",
|
|
"8",
|
|
"9",
|
|
]
|
|
`);
|
|
|
|
// Out of range indexes should be limited
|
|
const outOfRange = await ctx.redis.send("LRANGE", [key, "5", "100"]);
|
|
expect(Array.isArray(outOfRange)).toBe(true);
|
|
expect(outOfRange).toMatchInlineSnapshot(`
|
|
[
|
|
"5",
|
|
"6",
|
|
"7",
|
|
"8",
|
|
"9",
|
|
]
|
|
`);
|
|
});
|
|
|
|
test("LTRIM command", async () => {
|
|
const key = ctx.generateKey("ltrim-test");
|
|
|
|
// Set up test list with 10 elements
|
|
await ctx.redis.send("RPUSH", [key, "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]);
|
|
|
|
// Trim the list to keep only elements from index 2 to 7
|
|
// TODO: When a direct ltrim method is implemented, use that instead
|
|
const trimResult = await ctx.redis.send("LTRIM", [key, "2", "7"]);
|
|
expect(trimResult).toMatchInlineSnapshot(`"OK"`);
|
|
|
|
// Verify the trimmed list
|
|
const result = await ctx.redis.send("LRANGE", [key, "0", "-1"]);
|
|
expect(Array.isArray(result)).toBe(true);
|
|
expect(result).toMatchInlineSnapshot(`
|
|
[
|
|
"2",
|
|
"3",
|
|
"4",
|
|
"5",
|
|
"6",
|
|
"7",
|
|
]
|
|
`);
|
|
});
|
|
});
|
|
|
|
describe("List Information", () => {
|
|
test("LLEN command", async () => {
|
|
const key = ctx.generateKey("llen-test");
|
|
|
|
// Empty list should have length 0
|
|
const emptyLen = await ctx.redis.send("LLEN", [key]);
|
|
expectType<number>(emptyLen, "number");
|
|
expect(emptyLen).toBe(0);
|
|
|
|
// Add elements and check length
|
|
await ctx.redis.send("RPUSH", [key, "a", "b", "c", "d"]);
|
|
const len = await ctx.redis.send("LLEN", [key]);
|
|
expectType<number>(len, "number");
|
|
expect(len).toBe(4);
|
|
|
|
// Remove elements and check length
|
|
await ctx.redis.send("LPOP", [key, "2"]);
|
|
const updatedLen = await ctx.redis.send("LLEN", [key]);
|
|
expectType<number>(updatedLen, "number");
|
|
expect(updatedLen).toBe(2);
|
|
});
|
|
|
|
test("LINDEX command", async () => {
|
|
const key = ctx.generateKey("lindex-test");
|
|
|
|
// Set up test list
|
|
await ctx.redis.send("RPUSH", [key, "val0", "val1", "val2", "val3", "val4"]);
|
|
|
|
// Get element at index 0 (first element)
|
|
const firstElement = await ctx.redis.send("LINDEX", [key, "0"]);
|
|
expect(firstElement).toBe("val0");
|
|
|
|
// Get element at index 2 (middle element)
|
|
const middleElement = await ctx.redis.send("LINDEX", [key, "2"]);
|
|
expect(middleElement).toBe("val2");
|
|
|
|
// Get element at index -1 (last element)
|
|
const lastElement = await ctx.redis.send("LINDEX", [key, "-1"]);
|
|
expect(lastElement).toBe("val4");
|
|
|
|
// Get element at index -2 (second to last element)
|
|
const secondToLastElement = await ctx.redis.send("LINDEX", [key, "-2"]);
|
|
expect(secondToLastElement).toBe("val3");
|
|
|
|
// Get element at out of range index
|
|
const nonExistent = await ctx.redis.send("LINDEX", [key, "100"]);
|
|
expect(nonExistent).toBeNull();
|
|
});
|
|
|
|
test("LINSERT command", async () => {
|
|
const key = ctx.generateKey("linsert-test");
|
|
|
|
// Set up test list
|
|
await ctx.redis.send("RPUSH", [key, "one", "three", "four"]);
|
|
|
|
// Insert before a value
|
|
const beforeResult = await ctx.redis.send("LINSERT", [key, "BEFORE", "three", "two"]);
|
|
expectType<number>(beforeResult, "number");
|
|
expect(beforeResult).toBe(4); // New length is 4
|
|
|
|
// Insert after a value
|
|
const afterResult = await ctx.redis.send("LINSERT", [key, "AFTER", "four", "five"]);
|
|
expectType<number>(afterResult, "number");
|
|
expect(afterResult).toBe(5); // New length is 5
|
|
|
|
// Verify the list content
|
|
const content = await ctx.redis.send("LRANGE", [key, "0", "-1"]);
|
|
expect(Array.isArray(content)).toBe(true);
|
|
expect(content).toEqual(["one", "two", "three", "four", "five"]);
|
|
|
|
// Insert for non-existent pivot
|
|
const nonExistentResult = await ctx.redis.send("LINSERT", [key, "BEFORE", "nonexistent", "value"]);
|
|
expectType<number>(nonExistentResult, "number");
|
|
expect(nonExistentResult).toBe(-1); // -1 indicates pivot wasn't found
|
|
});
|
|
|
|
test("LSET command", async () => {
|
|
const key = ctx.generateKey("lset-test");
|
|
|
|
// Set up test list
|
|
await ctx.redis.send("RPUSH", [key, "a", "b", "c", "d"]);
|
|
|
|
// Set element at index 1
|
|
const setResult = await ctx.redis.send("LSET", [key, "1", "B"]);
|
|
expect(setResult).toBe("OK");
|
|
|
|
// Set element at last index
|
|
const lastSetResult = await ctx.redis.send("LSET", [key, "-1", "D"]);
|
|
expect(lastSetResult).toBe("OK");
|
|
|
|
// Verify the modified list
|
|
const content = await ctx.redis.send("LRANGE", [key, "0", "-1"]);
|
|
expect(Array.isArray(content)).toBe(true);
|
|
expect(content).toEqual(["a", "B", "c", "D"]);
|
|
|
|
// Setting out of range index should error
|
|
try {
|
|
await ctx.redis.send("LSET", [key, "100", "value"]);
|
|
// We should not reach here
|
|
expect(false).toBe(true);
|
|
} catch (error) {
|
|
// Expected error
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("List Position Operations", () => {
|
|
test("LPOS command", async () => {
|
|
const key = ctx.generateKey("lpos-test");
|
|
|
|
// Set up test list with duplicate elements
|
|
await ctx.redis.send("RPUSH", [key, "a", "b", "c", "d", "e", "a", "c", "a"]);
|
|
|
|
// Get first occurrence of "a"
|
|
const firstPos = await ctx.redis.send("LPOS", [key, "a"]);
|
|
expectType<number>(firstPos, "number");
|
|
expect(firstPos).toBe(0);
|
|
|
|
// Get first occurrence of "c"
|
|
const firstPosC = await ctx.redis.send("LPOS", [key, "c"]);
|
|
expectType<number>(firstPosC, "number");
|
|
expect(firstPosC).toBe(2);
|
|
|
|
// Get position of non-existent element
|
|
const nonExistentPos = await ctx.redis.send("LPOS", [key, "z"]);
|
|
expect(nonExistentPos).toBeNull();
|
|
|
|
// Get all occurrences of "a"
|
|
const allPosA = await ctx.redis.send("LPOS", [key, "a", "COUNT", "0"]);
|
|
expect(Array.isArray(allPosA)).toBe(true);
|
|
expect(allPosA).toEqual([0, 5, 7]);
|
|
|
|
// Get first 2 occurrences of "a"
|
|
const twoPos = await ctx.redis.send("LPOS", [key, "a", "COUNT", "2"]);
|
|
expect(Array.isArray(twoPos)).toBe(true);
|
|
expect(twoPos).toEqual([0, 5]);
|
|
|
|
// Get position of "a" starting from index 1
|
|
const posFromIndex = await ctx.redis.send("LPOS", [key, "a", "RANK", "2"]);
|
|
expectType<number>(posFromIndex, "number");
|
|
expect(posFromIndex).toBe(5);
|
|
});
|
|
});
|
|
|
|
describe("List Moving Operations", () => {
|
|
test("RPOPLPUSH command", async () => {
|
|
const source = ctx.generateKey("rpoplpush-source");
|
|
const destination = ctx.generateKey("rpoplpush-dest");
|
|
|
|
// Set up source list
|
|
await ctx.redis.send("RPUSH", [source, "one", "two", "three"]);
|
|
|
|
// Set up destination list
|
|
await ctx.redis.send("RPUSH", [destination, "a", "b"]);
|
|
|
|
// Move element from source to destination
|
|
const result = await ctx.redis.send("RPOPLPUSH", [source, destination]);
|
|
expect(result).toBe("three");
|
|
|
|
// Verify source list
|
|
const sourceContent = await ctx.redis.send("LRANGE", [source, "0", "-1"]);
|
|
expect(Array.isArray(sourceContent)).toBe(true);
|
|
expect(sourceContent).toEqual(["one", "two"]);
|
|
|
|
// Verify destination list
|
|
const destContent = await ctx.redis.send("LRANGE", [destination, "0", "-1"]);
|
|
expect(Array.isArray(destContent)).toBe(true);
|
|
expect(destContent).toEqual(["three", "a", "b"]);
|
|
});
|
|
|
|
test("LMOVE command", async () => {
|
|
const source = ctx.generateKey("lmove-source");
|
|
const destination = ctx.generateKey("lmove-dest");
|
|
|
|
// Set up source list
|
|
await ctx.redis.send("RPUSH", [source, "one", "two", "three"]);
|
|
|
|
// Set up destination list
|
|
await ctx.redis.send("RPUSH", [destination, "a", "b"]);
|
|
|
|
// Right to left move
|
|
try {
|
|
const rtlResult = await ctx.redis.send("LMOVE", [source, destination, "RIGHT", "LEFT"]);
|
|
expect(rtlResult).toBe("three");
|
|
|
|
// Left to right move
|
|
const ltrResult = await ctx.redis.send("LMOVE", [source, destination, "LEFT", "RIGHT"]);
|
|
expect(ltrResult).toBe("one");
|
|
|
|
// Verify source list
|
|
const sourceContent = await ctx.redis.send("LRANGE", [source, "0", "-1"]);
|
|
expect(Array.isArray(sourceContent)).toBe(true);
|
|
expect(sourceContent).toEqual(["two"]);
|
|
|
|
// Verify destination list
|
|
const destContent = await ctx.redis.send("LRANGE", [destination, "0", "-1"]);
|
|
expect(Array.isArray(destContent)).toBe(true);
|
|
expect(destContent).toEqual(["three", "a", "b", "one"]);
|
|
} catch (error) {
|
|
// Some Redis versions might not support LMOVE
|
|
console.warn("LMOVE command not supported, skipping test");
|
|
}
|
|
});
|
|
});
|
|
|
|
describe.skipIf(isCI)("Blocking Operations", () => {
|
|
// Note: These tests can be problematic in automated test suites
|
|
// due to the blocking nature. We'll implement with very short timeouts.
|
|
test("BLPOP with timeout", async () => {
|
|
const key = ctx.generateKey("blpop-test");
|
|
|
|
// Try to pop from an empty list with 1 second timeout
|
|
const timeoutResult = await ctx.redis.send("BLPOP", [key, "1"]);
|
|
expect(timeoutResult).toBeNull(); // Should timeout and return null
|
|
|
|
// Add elements and then try again
|
|
await ctx.redis.send("RPUSH", [key, "value1", "value2"]);
|
|
|
|
// Now the BLPOP should immediately return
|
|
const result = await ctx.redis.send("BLPOP", [key, "1"]);
|
|
expect(Array.isArray(result)).toBe(true);
|
|
expect(result.length).toBe(2);
|
|
expect(result[0]).toBe(key);
|
|
expect(result[1]).toBe("value1");
|
|
});
|
|
|
|
test("BRPOP with timeout", async () => {
|
|
const key = ctx.generateKey("brpop-test");
|
|
|
|
// Try to pop from an empty list with 1 second timeout
|
|
const timeoutResult = await ctx.redis.send("BRPOP", [key, "1"]);
|
|
expect(timeoutResult).toBeNull(); // Should timeout and return null
|
|
|
|
// Add elements and then try again
|
|
await ctx.redis.send("RPUSH", [key, "value1", "value2"]);
|
|
|
|
// Now the BRPOP should immediately return
|
|
const result = await ctx.redis.send("BRPOP", [key, "1"]);
|
|
expect(Array.isArray(result)).toBe(true);
|
|
expect(result.length).toBe(2);
|
|
expect(result[0]).toBe(key);
|
|
expect(result[1]).toBe("value2");
|
|
});
|
|
|
|
test("BRPOPLPUSH with timeout", async () => {
|
|
const source = ctx.generateKey("brpoplpush-source");
|
|
const destination = ctx.generateKey("brpoplpush-dest");
|
|
|
|
// Try with empty source and 1 second timeout
|
|
const timeoutResult = await ctx.redis.send("BRPOPLPUSH", [source, destination, "1"]);
|
|
expect(timeoutResult).toBeNull(); // Should timeout and return null
|
|
|
|
// Set up source and destination
|
|
await ctx.redis.send("RPUSH", [source, "value1", "value2"]);
|
|
await ctx.redis.send("RPUSH", [destination, "a", "b"]);
|
|
|
|
// Now should immediately return
|
|
const result = await ctx.redis.send("BRPOPLPUSH", [source, destination, "1"]);
|
|
expect(result).toBe("value2");
|
|
|
|
// Verify destination received the element
|
|
const destContent = await ctx.redis.send("LRANGE", [destination, "0", "-1"]);
|
|
expect(Array.isArray(destContent)).toBe(true);
|
|
expect(destContent).toEqual(["value2", "a", "b"]);
|
|
});
|
|
});
|
|
});
|