From 4e1b00898b3aec7813a2240ab8fa3cf30b080bff Mon Sep 17 00:00:00 2001 From: Hao Liu <44379968+TheRealHaoLiu@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:22:47 -0400 Subject: [PATCH] Complete the FakeRedis test double for the subsystem metrics pipe (#16745) TestCallbackBrokerWorker::test_duplicate_key_not_saved_twice can crash with AttributeError: 'FakeRedis' object has no attribute 'hincrby'. CallbackBrokerWorker.flush() ends with if self.subsystem_metrics.should_pipe_execute() is True: self.subsystem_metrics.pipe_execute() and should_pipe_execute() is True once more than SUBSYSTEM_METRICS_INTERVAL_SAVE_TO_REDIS seconds have passed since the last pipe execute. On a fast machine that interval never elapses during the test, so the pipe is never executed and nothing notices; on a slow one it does, pipe_execute() calls store_value() on every metric, and IntM/FloatM/SetIntM reach for hincrby/hincrbyfloat/hset, which the FakeRedis double in awx/main/tests/functional/conftest.py does not have. Rather than suppress the pipe execute in the one test, make the double complete enough to execute it: add the hash commands the metric store_value() implementations use, an execute() for the pipeline (pipeline() returns self), and a lock() returning a lock that never acquires, so send_metrics() takes its "another thread holds the lock" early return instead of trying to emit a websocket notification through a channel layer that is not available under test. --- awx/main/tests/functional/conftest.py | 28 +++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index bd3e9a0178..659442dfb2 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -76,6 +76,16 @@ def swagger_autogen(requests=__SWAGGER_REQUESTS__): return requests +class FakeLock: + # Never acquirable, so callers that guard work with a redis lock take the + # "someone else holds it" branch instead of reaching for real services. + def acquire(self, *args, **kwargs): + return False + + def release(self, *args, **kwargs): + pass + + class FakeRedis: def __init__(self, *args, **kwargs): # Accept and ignore all arguments to match redis.Redis signature @@ -90,6 +100,21 @@ class FakeRedis: def get(self, *args, **kwargs): return None + def hset(self, *args, **kwargs): + return 1 + + def hget(self, *args, **kwargs): + return None + + def hincrby(self, *args, **kwargs): + return 0 + + def hincrbyfloat(self, *args, **kwargs): + return 0.0 + + def lock(self, *args, **kwargs): + return FakeLock() + def rpush(self, *args, **kwargs): return 1 @@ -112,6 +137,9 @@ class FakeRedis: def pipeline(self): return self + def execute(self, *args, **kwargs): + return [] + def ping(self): return