|
| 1 | +// |
| 2 | +// RedisDashboardCommandTests.swift |
| 3 | +// TableProTests |
| 4 | +// |
| 5 | +// The Server Dashboard reads SLOWLOG GET as a grid and sends every command as OUTSIDEBLOCK, so a |
| 6 | +// refresh is held back while the user has a MULTI block open instead of joining it. |
| 7 | +// |
| 8 | + |
| 9 | +import Foundation |
| 10 | +import TableProPluginKit |
| 11 | +import TableProRedisCore |
| 12 | +import Testing |
| 13 | + |
| 14 | +struct RedisSlowLogParseTests { |
| 15 | + @Test("SLOWLOG GET, with or without a count, is typed", arguments: [ |
| 16 | + ("SLOWLOG GET", nil), |
| 17 | + ("slowlog get 20", 20), |
| 18 | + ] as [(String, Int?)]) |
| 19 | + func slowLogGetIsTyped(statement: String, count: Int?) throws { |
| 20 | + guard case .slowlogGet(let parsed) = try RedisCommandParser.parse(statement) else { |
| 21 | + Issue.record("Expected .slowlogGet for \(statement)") |
| 22 | + return |
| 23 | + } |
| 24 | + #expect(parsed == count) |
| 25 | + } |
| 26 | + |
| 27 | + @Test("Any other SLOWLOG form goes to the server as typed", arguments: [ |
| 28 | + "SLOWLOG LEN", "SLOWLOG RESET", "SLOWLOG GET abc", "SLOWLOG GET 1 2", "SLOWLOG", |
| 29 | + ]) |
| 30 | + func otherFormsAreVerbatim(statement: String) throws { |
| 31 | + guard case .command = try RedisCommandParser.parse(statement) else { |
| 32 | + Issue.record("Expected .command for \(statement)") |
| 33 | + return |
| 34 | + } |
| 35 | + } |
| 36 | +} |
| 37 | + |
| 38 | +struct RedisOutsideBlockParseTests { |
| 39 | + @Test("OUTSIDEBLOCK keeps the grid of INFO and SLOWLOG GET") |
| 40 | + func keepsTypedServerGrids() throws { |
| 41 | + guard case .outsideBlock(.info(let sections)) = try RedisCommandParser.parse("OUTSIDEBLOCK INFO") else { |
| 42 | + Issue.record("Expected INFO outside the block") |
| 43 | + return |
| 44 | + } |
| 45 | + #expect(sections.isEmpty) |
| 46 | + guard case .outsideBlock(.slowlogGet(let count)) = try RedisCommandParser.parse("OUTSIDEBLOCK SLOWLOG GET 20") else { |
| 47 | + Issue.record("Expected SLOWLOG GET outside the block") |
| 48 | + return |
| 49 | + } |
| 50 | + #expect(count == 20) |
| 51 | + } |
| 52 | + |
| 53 | + @Test("Any other command is sent as typed", arguments: [ |
| 54 | + "OUTSIDEBLOCK CLIENT LIST", "OUTSIDEBLOCK CLIENT KILL ID 21", "outsideblock GET key", |
| 55 | + ]) |
| 56 | + func otherCommandsAreVerbatim(statement: String) throws { |
| 57 | + guard case .outsideBlock(.command(let args)) = try RedisCommandParser.parse(statement) else { |
| 58 | + Issue.record("Expected a verbatim command outside the block for \(statement)") |
| 59 | + return |
| 60 | + } |
| 61 | + #expect(args.map { $0.text } == Array(statement.split(separator: " ").dropFirst().map(String.init))) |
| 62 | + } |
| 63 | + |
| 64 | + @Test("OUTSIDEBLOCK with nothing after it is refused") |
| 65 | + func emptyIsRefused() { |
| 66 | + #expect(throws: RedisParseError.self) { try RedisCommandParser.parse("OUTSIDEBLOCK") } |
| 67 | + } |
| 68 | +} |
| 69 | + |
| 70 | +struct RedisSlowLogGridTests { |
| 71 | + @Test("An entry becomes one row, its arguments quoted the way the console reads them") |
| 72 | + func entryBecomesARow() { |
| 73 | + let reply = RedisReply.array([ |
| 74 | + .array([ |
| 75 | + .integer(9), .integer(1_792_537_207), .integer(626), |
| 76 | + .array([.string("SET"), .string("foo"), .string("a b,c]")]), |
| 77 | + .string("127.0.0.1:59713"), .string("worker"), .integer(3), |
| 78 | + ]), |
| 79 | + ]) |
| 80 | + let grid = RedisReplyGrid.slowLog(reply) |
| 81 | + #expect(grid.columns == RedisSlowLogGrid.columns) |
| 82 | + #expect(grid.columnTypeNames.count == grid.columns.count) |
| 83 | + #expect(grid.rows == [[ |
| 84 | + .text("9"), .text("1792537207"), .text("626"), .text(#"SET foo "a b,c]""#), |
| 85 | + .text("127.0.0.1:59713"), .text("worker"), |
| 86 | + ]]) |
| 87 | + } |
| 88 | + |
| 89 | + @Test("Redis before 4.0 sends no client, and a malformed entry is skipped") |
| 90 | + func olderAndMalformedEntries() { |
| 91 | + let reply = RedisReply.array([ |
| 92 | + .array([.integer(1), .integer(1_500_000_000), .integer(20_000), .array([.string("KEYS"), .string("*")])]), |
| 93 | + .array([.integer(2)]), |
| 94 | + .string("not an entry"), |
| 95 | + ]) |
| 96 | + #expect(RedisReplyGrid.slowLog(reply).rows == [[ |
| 97 | + .text("1"), .text("1500000000"), .text("20000"), .text("KEYS *"), .null, .null, |
| 98 | + ]]) |
| 99 | + } |
| 100 | +} |
| 101 | + |
| 102 | +struct RedisOutsideBlockExecutionTests { |
| 103 | + private func driver() -> RedisPluginDriver { |
| 104 | + RedisPluginDriver(config: DriverConnectionConfig( |
| 105 | + host: "127.0.0.1", port: 6_379, username: "", password: "", database: "0" |
| 106 | + )) |
| 107 | + } |
| 108 | + |
| 109 | + @Test("A dashboard command runs outside the block scope") |
| 110 | + func runsOutsideTheBlock() async throws { |
| 111 | + let channel = StubRedisChannel([.string("id=21 addr=10.0.0.4:51022 cmd=get")]) |
| 112 | + let result = try await driver().executeOperation( |
| 113 | + try RedisCommandParser.parse("OUTSIDEBLOCK CLIENT LIST"), connection: channel, startTime: Date() |
| 114 | + ) |
| 115 | + #expect(channel.sentCommands == [["CLIENT", "LIST"]]) |
| 116 | + #expect(channel.sentScopes == [.outsideBlock]) |
| 117 | + #expect(result.rows.first?.first?.asText?.hasPrefix("id=21") == true) |
| 118 | + } |
| 119 | + |
| 120 | + @Test("With the user's MULTI block open the command is held back, never queued into it") |
| 121 | + func heldBackWhileTheBlockIsOpen() async throws { |
| 122 | + let channel = StubRedisChannel([]) |
| 123 | + channel.observeOpenBlock() |
| 124 | + await #expect(throws: RedisHeldBackCommand(command: "INFO", held: .openBlock)) { |
| 125 | + _ = try await driver().executeOperation( |
| 126 | + try RedisCommandParser.parse("OUTSIDEBLOCK INFO"), connection: channel, startTime: Date() |
| 127 | + ) |
| 128 | + } |
| 129 | + #expect(channel.sentCommands.isEmpty) |
| 130 | + } |
| 131 | + |
| 132 | + @Test("The same command typed by the user still joins the user's block") |
| 133 | + func typedCommandStillQueues() async throws { |
| 134 | + let channel = StubRedisChannel([.status("QUEUED")]) |
| 135 | + channel.observeOpenBlock() |
| 136 | + let result = try await driver().executeOperation( |
| 137 | + try RedisCommandParser.parse("SLOWLOG GET 5"), connection: channel, startTime: Date() |
| 138 | + ) |
| 139 | + #expect(channel.sentScopes == [.session]) |
| 140 | + #expect(result.rows.first?.first?.asText == "QUEUED") |
| 141 | + } |
| 142 | +} |
0 commit comments