Merge SubmissionEntries based on similarity and on patch mitigation (#899)

* Use an internal_patch_id instead of indices

This is the first step in being able to merge sets of PoVs and test
patches against all PoVs within a task.

* Discard redundant builds

* Initial PoV-merging strategy

Still not optimal in terms of SARIF-matching/bundling etc

* Appears to be working version of merging including bundle and sarif handling

* Make tests pass

* Update integration test steps

* Fixes and cleanup from review

* Removed additional request for patched builds

* Refactored some loops into find-style functions to simplify

* Inline small function used once

* Refactors for increased robustness and readability including additional testing

* SARIF matching - additional tests and refactor

* Add enumerate_task_submissions

* Refactor and simplify tests using a builder

Cleanup unused code

* Fix read_submissions to use CrashWithId

* Improvements based on review

* Cache final states of PoV reproduce (#909)

As these never change we can limit the load on redis by caching the
results.

* Merge SubmissionEntries based on patches (#910)

* Cache final states of PoV reproduce

As these never change we can limit the load on redis by caching the
results.

* Merge SubmissionEntries based on patches

If a PoV in another entry is mitigated by the current entry's patch,
merge the entries as athey should be considered the same
ChallengeVulnerability.

* Add positional argument

* Hold of submitting a patch while evaluating

Check each already submitted patch before submitting a new one for the
same task. If any of the already submitted patches mitigates any PoV in
the current SubmissionEntry - do not submit this. It will be merged
later on.

* Additional logging, truncate ids

* Only request patch if no submitted patch mitigates

Before we request a new patch, we check each of the already submitted
patches to see if any of them already mitigates the PoVs in the current
SubmissionEntry. If they do, this will be merged at a later stage.

* PR feedback
This commit is contained in:
Henrik Brodin
2025-06-22 12:36:37 +02:00
committed by GitHub
parent 9e1969c10b
commit 00a7a4be33
10 changed files with 5281 additions and 1079 deletions
+14 -14
View File
@@ -148,13 +148,13 @@ jobs:
- name: Wait for vuln to be found
timeout-minutes: ${{ fromJSON(env.VULN_TIMEOUT) }}
run: |
while ! kubectl logs -n $BUTTERCUP_NAMESPACE -l app=scheduler --tail=-1 | grep "] SUBMIT_PATCH_REQUEST pov_id=" ; do
while ! kubectl logs -n $BUTTERCUP_NAMESPACE -l app=scheduler --tail=-1 | grep "] POV submission response: pov_id=" ; do
sleep 60
done
- name: Wait for vuln to enter the passed state in competition api
timeout-minutes: ${{ fromJSON(env.VULN_TIMEOUT) }}
run: |
while ! kubectl logs -n $BUTTERCUP_NAMESPACE -l app=scheduler --tail=-1 | grep "WAIT_POV_PASS -> SUBMIT_PATCH" ; do
while ! kubectl logs -n $BUTTERCUP_NAMESPACE -l app=scheduler --tail=-1 | grep "Updated POV status. New status PASSED" ; do
sleep 60
done
@@ -165,30 +165,30 @@ jobs:
sleep 60
done
- name: Wait for patch to be submitted
- name: Wait for patch to be recorded
timeout-minutes: ${{ fromJSON(env.PATCH_TIMEOUT) }}
run: |
while ! kubectl logs -n $BUTTERCUP_NAMESPACE -l app=scheduler --tail=-1 | grep "SUBMIT_PATCH -> WAIT_PATCH_PASS"; do
while ! kubectl logs -n $BUTTERCUP_NAMESPACE -l app=scheduler --tail=-1 | grep "Appending patch for task"; do
sleep 60
done
- name: Wait for patch to enter the passed state in competition api
timeout-minutes: ${{ fromJSON(env.PATCH_TIMEOUT) }}
run: |
while ! kubectl logs -n $BUTTERCUP_NAMESPACE -l app=scheduler --tail=-1 | grep "WAIT_PATCH_PASS -> SUBMIT_BUNDLE"; do
while ! kubectl logs -n $BUTTERCUP_NAMESPACE -l app=scheduler --tail=-1 | grep "Patch passed"; do
sleep 60
done
- name: Wait for bundle to be submitted
timeout-minutes: ${{ fromJSON(env.BUNDLE_TIMEOUT) }}
run: |
while ! kubectl logs -n $BUTTERCUP_NAMESPACE -l app=scheduler --tail=-1 | grep "SUBMIT_BUNDLE -> SUBMIT_MATCHING_SARIF"; do
while ! kubectl logs -n $BUTTERCUP_NAMESPACE -l app=scheduler --tail=-1 | grep "Bundle submission response: bundle_id="; do
sleep 60
done
- name: Send a SARIF broadcast
timeout-minutes: ${{ fromJSON(env.SARIF_TIMEOUT) }}
run: |
TASK_ID=$(kubectl logs -n $BUTTERCUP_NAMESPACE -l app=scheduler --tail=-1 | grep "SUBMIT_BUNDLE -> SUBMIT_MATCHING_SARIF" | grep -o "\[[^]]*\]" | grep -o "[^[]*$" | cut -d: -f3 | tr -d ']')
TASK_ID=$(kubectl logs -n $BUTTERCUP_NAMESPACE -l app=scheduler --tail=-1 | grep "Submitting bundle for harness" | grep -o "\[[^]]*\]" | grep -o "[^[]*$" | cut -d: -f3 | tr -d ']')
echo "Task ID: $TASK_ID"
while true; do
# Kill any existing port-forward processes
@@ -209,16 +209,16 @@ jobs:
# Failed - wait a bit before retrying
sleep 10
done
- name: Wait for SARIF to be submitted as correct
timeout-minutes: ${{ fromJSON(env.SARIF_TIMEOUT) }}
run: |
while ! kubectl logs -n $BUTTERCUP_NAMESPACE -l app=scheduler --tail=-1 | grep "SUBMIT_MATCHING_SARIF -> SUBMIT_BUNDLE_PATCH"; do
sleep 60
done
- name: Wait for Bundle to be patched to include the SARIF
timeout-minutes: ${{ fromJSON(env.SARIF_TIMEOUT) }}
run: |
while ! kubectl logs -n $BUTTERCUP_NAMESPACE -l app=scheduler --tail=-1 | grep "SUBMIT_BUNDLE_PATCH -> STOP"; do
while ! kubectl logs -n $BUTTERCUP_NAMESPACE -l app=scheduler --tail=-1 | grep "Bundle patch submission response: broadcast_sarif_id="; do
sleep 60
done
- name: Wait for SARIF to be submitted as correct
timeout-minutes: ${{ fromJSON(env.SARIF_TIMEOUT) }}
run: |
while ! kubectl logs -n $BUTTERCUP_NAMESPACE -l app=scheduler --tail=-1 | grep "Matching SARIF submission response"; do
sleep 60
done
+34 -21
View File
@@ -137,40 +137,53 @@ message FunctionCoverage {
int32 covered_lines = 4;
}
enum SubmissionResult {
NONE = 0; // Default/unset state - no submission result yet
ACCEPTED = 1;
PASSED = 2;
FAILED = 3;
DEADLINE_EXCEEDED = 4;
ERRORED = 5;
INCONCLUSIVE = 6;
}
message SubmissionEntryPatch {
string patch = 1;
string internal_patch_id = 2;
string competition_patch_id = 3;
repeated BuildOutput build_outputs = 4;
optional SubmissionResult result = 5;
}
message SubmissionEntry {
enum SubmissionState {
SUBMIT_PATCH_REQUEST = 0;
WAIT_POV_PASS = 1;
SUBMIT_PATCH = 2;
WAIT_PATCH_PASS = 3;
SUBMIT_BUNDLE = 4;
SUBMIT_MATCHING_SARIF = 5;
SUBMIT_BUNDLE_PATCH = 6;
STOP = 7;
}
SubmissionState state = 1;
repeated TracedCrash crashes = 2;
string pov_id = 3;
string competition_patch_id = 4;
message Bundle {
string task_id = 1;
string competition_pov_id = 2;
string competition_patch_id = 3;
string competition_sarif_id = 4;
string bundle_id = 5;
string sarif_id = 6;
}
message CrashWithId {
TracedCrash crash = 1;
string competition_pov_id = 2;
optional SubmissionResult result = 3;
}
// This is our model of a Challenge Vulnerability
message SubmissionEntry {
bool stop = 1;
repeated CrashWithId crashes = 2;
repeated Bundle bundles = 3;
// Content of patches targeting this vulnerability
repeated SubmissionEntryPatch patches = 7;
repeated SubmissionEntryPatch patches = 4;
// Current patch being considered
int32 patch_idx = 8;
int32 patch_submission_attempt = 9;
int32 patch_idx = 5;
int32 patch_submission_attempts = 6;
}
message POVReproduceRequest {
File diff suppressed because one or more lines are too long
@@ -4,9 +4,16 @@ from google.protobuf import descriptor as _descriptor
from google.protobuf import message as _message
from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union
ACCEPTED: SubmissionResult
COVERAGE: BuildType
DEADLINE_EXCEEDED: SubmissionResult
DESCRIPTOR: _descriptor.FileDescriptor
ERRORED: SubmissionResult
FAILED: SubmissionResult
FUZZER: BuildType
INCONCLUSIVE: SubmissionResult
NONE: SubmissionResult
PASSED: SubmissionResult
PATCH: BuildType
TRACER_NO_DIFF: BuildType
@@ -48,6 +55,20 @@ class BuildRequest(_message.Message):
task_id: str
def __init__(self, engine: _Optional[str] = ..., sanitizer: _Optional[str] = ..., task_dir: _Optional[str] = ..., task_id: _Optional[str] = ..., build_type: _Optional[_Union[BuildType, str]] = ..., apply_diff: bool = ..., patch: _Optional[str] = ..., internal_patch_id: _Optional[str] = ...) -> None: ...
class Bundle(_message.Message):
__slots__ = ["bundle_id", "competition_patch_id", "competition_pov_id", "competition_sarif_id", "task_id"]
BUNDLE_ID_FIELD_NUMBER: _ClassVar[int]
COMPETITION_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
COMPETITION_POV_ID_FIELD_NUMBER: _ClassVar[int]
COMPETITION_SARIF_ID_FIELD_NUMBER: _ClassVar[int]
TASK_ID_FIELD_NUMBER: _ClassVar[int]
bundle_id: str
competition_patch_id: str
competition_pov_id: str
competition_sarif_id: str
task_id: str
def __init__(self, task_id: _Optional[str] = ..., competition_pov_id: _Optional[str] = ..., competition_patch_id: _Optional[str] = ..., competition_sarif_id: _Optional[str] = ..., bundle_id: _Optional[str] = ...) -> None: ...
class ConfirmedVulnerability(_message.Message):
__slots__ = ["crashes", "internal_patch_id"]
CRASHES_FIELD_NUMBER: _ClassVar[int]
@@ -70,6 +91,16 @@ class Crash(_message.Message):
target: BuildOutput
def __init__(self, target: _Optional[_Union[BuildOutput, _Mapping]] = ..., harness_name: _Optional[str] = ..., crash_input_path: _Optional[str] = ..., stacktrace: _Optional[str] = ..., crash_token: _Optional[str] = ...) -> None: ...
class CrashWithId(_message.Message):
__slots__ = ["competition_pov_id", "crash", "result"]
COMPETITION_POV_ID_FIELD_NUMBER: _ClassVar[int]
CRASH_FIELD_NUMBER: _ClassVar[int]
RESULT_FIELD_NUMBER: _ClassVar[int]
competition_pov_id: str
crash: TracedCrash
result: SubmissionResult
def __init__(self, crash: _Optional[_Union[TracedCrash, _Mapping]] = ..., competition_pov_id: _Optional[str] = ..., result: _Optional[_Union[SubmissionResult, str]] = ...) -> None: ...
class FunctionCoverage(_message.Message):
__slots__ = ["covered_lines", "function_name", "function_paths", "total_lines"]
COVERED_LINES_FIELD_NUMBER: _ClassVar[int]
@@ -158,48 +189,34 @@ class SourceDetail(_message.Message):
def __init__(self, sha256: _Optional[str] = ..., source_type: _Optional[_Union[SourceDetail.SourceType, str]] = ..., url: _Optional[str] = ...) -> None: ...
class SubmissionEntry(_message.Message):
__slots__ = ["bundle_id", "competition_patch_id", "crashes", "patch_idx", "patch_submission_attempt", "patches", "pov_id", "sarif_id", "state"]
class SubmissionState(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = []
BUNDLE_ID_FIELD_NUMBER: _ClassVar[int]
COMPETITION_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
__slots__ = ["bundles", "crashes", "patch_idx", "patch_submission_attempts", "patches", "stop"]
BUNDLES_FIELD_NUMBER: _ClassVar[int]
CRASHES_FIELD_NUMBER: _ClassVar[int]
PATCHES_FIELD_NUMBER: _ClassVar[int]
PATCH_IDX_FIELD_NUMBER: _ClassVar[int]
PATCH_SUBMISSION_ATTEMPT_FIELD_NUMBER: _ClassVar[int]
POV_ID_FIELD_NUMBER: _ClassVar[int]
SARIF_ID_FIELD_NUMBER: _ClassVar[int]
STATE_FIELD_NUMBER: _ClassVar[int]
STOP: SubmissionEntry.SubmissionState
SUBMIT_BUNDLE: SubmissionEntry.SubmissionState
SUBMIT_BUNDLE_PATCH: SubmissionEntry.SubmissionState
SUBMIT_MATCHING_SARIF: SubmissionEntry.SubmissionState
SUBMIT_PATCH: SubmissionEntry.SubmissionState
SUBMIT_PATCH_REQUEST: SubmissionEntry.SubmissionState
WAIT_PATCH_PASS: SubmissionEntry.SubmissionState
WAIT_POV_PASS: SubmissionEntry.SubmissionState
bundle_id: str
competition_patch_id: str
crashes: _containers.RepeatedCompositeFieldContainer[TracedCrash]
PATCH_SUBMISSION_ATTEMPTS_FIELD_NUMBER: _ClassVar[int]
STOP_FIELD_NUMBER: _ClassVar[int]
bundles: _containers.RepeatedCompositeFieldContainer[Bundle]
crashes: _containers.RepeatedCompositeFieldContainer[CrashWithId]
patch_idx: int
patch_submission_attempt: int
patch_submission_attempts: int
patches: _containers.RepeatedCompositeFieldContainer[SubmissionEntryPatch]
pov_id: str
sarif_id: str
state: SubmissionEntry.SubmissionState
def __init__(self, state: _Optional[_Union[SubmissionEntry.SubmissionState, str]] = ..., crashes: _Optional[_Iterable[_Union[TracedCrash, _Mapping]]] = ..., pov_id: _Optional[str] = ..., competition_patch_id: _Optional[str] = ..., bundle_id: _Optional[str] = ..., sarif_id: _Optional[str] = ..., patches: _Optional[_Iterable[_Union[SubmissionEntryPatch, _Mapping]]] = ..., patch_idx: _Optional[int] = ..., patch_submission_attempt: _Optional[int] = ...) -> None: ...
stop: bool
def __init__(self, stop: bool = ..., crashes: _Optional[_Iterable[_Union[CrashWithId, _Mapping]]] = ..., bundles: _Optional[_Iterable[_Union[Bundle, _Mapping]]] = ..., patches: _Optional[_Iterable[_Union[SubmissionEntryPatch, _Mapping]]] = ..., patch_idx: _Optional[int] = ..., patch_submission_attempts: _Optional[int] = ...) -> None: ...
class SubmissionEntryPatch(_message.Message):
__slots__ = ["build_outputs", "competition_patch_id", "internal_patch_id", "patch"]
__slots__ = ["build_outputs", "competition_patch_id", "internal_patch_id", "patch", "result"]
BUILD_OUTPUTS_FIELD_NUMBER: _ClassVar[int]
COMPETITION_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
INTERNAL_PATCH_ID_FIELD_NUMBER: _ClassVar[int]
PATCH_FIELD_NUMBER: _ClassVar[int]
RESULT_FIELD_NUMBER: _ClassVar[int]
build_outputs: _containers.RepeatedCompositeFieldContainer[BuildOutput]
competition_patch_id: str
internal_patch_id: str
patch: str
def __init__(self, patch: _Optional[str] = ..., internal_patch_id: _Optional[str] = ..., competition_patch_id: _Optional[str] = ..., build_outputs: _Optional[_Iterable[_Union[BuildOutput, _Mapping]]] = ...) -> None: ...
result: SubmissionResult
def __init__(self, patch: _Optional[str] = ..., internal_patch_id: _Optional[str] = ..., competition_patch_id: _Optional[str] = ..., build_outputs: _Optional[_Iterable[_Union[BuildOutput, _Mapping]]] = ..., result: _Optional[_Union[SubmissionResult, str]] = ...) -> None: ...
class Task(_message.Message):
__slots__ = ["cancelled", "deadline", "focus", "message_id", "message_time", "metadata", "project_name", "sources", "task_id", "task_type"]
@@ -280,3 +297,6 @@ class WeightedHarness(_message.Message):
class BuildType(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = []
class SubmissionResult(int, metaclass=_enum_type_wrapper.EnumTypeWrapper):
__slots__ = []
+31
View File
@@ -5,6 +5,7 @@ from bson.json_util import dumps, CANONICAL_JSON_OPTIONS
from contextlib import contextmanager
import random
import json
from functools import lru_cache
# Import POVReproduceRequest for the refactored PoVReproduceStatus
from buttercup.common.datastructures.msg_pb2 import POVReproduceRequest, POVReproduceResponse
@@ -105,6 +106,28 @@ class PoVReproduceStatus:
def __init__(self, redis: Redis):
self.redis = redis
@lru_cache(maxsize=1000)
def _did_crash(self, key: str) -> bool | None:
"""Check if POV crashed (is in final states) and return crash status.
Args:
key: The serialized key for the POV reproduction request
Returns:
False if mitigated (didn't crash), True if non-mitigated (did crash), None if not in final states
"""
pipeline = self.redis.pipeline()
pipeline.sismember(POV_REPRODUCE_MITIGATED_SET_NAME, key)
pipeline.sismember(POV_REPRODUCE_NON_MITIGATED_SET_NAME, key)
result = pipeline.execute()
if result[0]:
return False # Mitigated - didn't crash
elif result[1]:
return True # Non-mitigated - did crash
else:
return None # Not in final states
def _make_key(self, request: POVReproduceRequest) -> str:
"""Create a unique key from a POVReproduceRequest by serializing it to string."""
return dumps(
@@ -122,11 +145,19 @@ class PoVReproduceStatus:
None if pending, POVReproduceResponse if completed
"""
key = self._make_key(request)
# First check cache for final states only
did_crash = self._did_crash(key)
if did_crash is not None:
return POVReproduceResponse(request=request, did_crash=did_crash)
# If not in final states, do the regular logic including pending check
pipeline = self.redis.pipeline()
pipeline.sismember(POV_REPRODUCE_PENDING_SET_NAME, key)
pipeline.sismember(POV_REPRODUCE_MITIGATED_SET_NAME, key)
pipeline.sismember(POV_REPRODUCE_NON_MITIGATED_SET_NAME, key)
result = pipeline.execute()
if result[0]:
return None # Pending
elif result[1]:
+2 -1
View File
@@ -29,7 +29,8 @@ def truncate_stacktraces(submission: SubmissionEntry, max_length: int = 80) -> S
text_format.Parse(submission_text, truncated_submission)
# Now truncate the stacktraces and crash token
for crash in truncated_submission.crashes:
for crash_with_id in truncated_submission.crashes:
crash = crash_with_id.crash
if crash.crash.stacktrace and len(crash.crash.stacktrace) > max_length:
crash.crash.stacktrace = crash.crash.stacktrace[:max_length] + "... (truncated)"
+337
View File
@@ -2,6 +2,7 @@ import pytest
from redis import Redis
from buttercup.common.sets import RedisSet, PoVReproduceStatus
from buttercup.common.datastructures.msg_pb2 import POVReproduceRequest, POVReproduceResponse
from unittest.mock import MagicMock
@pytest.fixture
@@ -558,3 +559,339 @@ class TestPoVReproduceStatus:
# Should have 2 pending items now (requests[4] and requests[5])
pending1 = pov_status.get_one_pending()
assert pending1.task_id in [requests[4].task_id, requests[5].task_id]
class TestPoVReproduceStatusCaching:
"""Test suite for PoVReproduceStatus caching functionality."""
def _create_mock_redis(self):
"""Helper to create properly mocked Redis client with pipeline support."""
mock_redis = MagicMock()
mock_pipeline = MagicMock()
mock_redis.pipeline.return_value = mock_pipeline
mock_pipeline.__enter__.return_value = mock_pipeline
mock_pipeline.__exit__.return_value = None
return mock_redis, mock_pipeline
def test_cache_hit_for_mitigated_status(self):
"""Test that repeated requests for mitigated status hit cache, not Redis."""
# Create mock Redis client
mock_redis, mock_pipeline = self._create_mock_redis()
# Mock the final states check to return mitigated
mock_pipeline.execute.return_value = [True, False] # mitigated=True, non_mitigated=False
pov_status = PoVReproduceStatus(mock_redis)
# Create sample request
request = POVReproduceRequest()
request.task_id = "test-task"
request.internal_patch_id = "0"
request.pov_path = "/test/path"
request.sanitizer = "asan"
request.harness_name = "test_harness"
# First call should hit Redis
result1 = pov_status.request_status(request)
assert isinstance(result1, POVReproduceResponse)
assert result1.did_crash is False
# Second call should hit cache, not Redis
result2 = pov_status.request_status(request)
assert isinstance(result2, POVReproduceResponse)
assert result2.did_crash is False
# Third call should also hit cache
result3 = pov_status.request_status(request)
assert isinstance(result3, POVReproduceResponse)
assert result3.did_crash is False
# Redis should only be called once (for the first request)
assert mock_redis.pipeline.call_count == 1
def test_cache_hit_for_non_mitigated_status(self):
"""Test that repeated requests for non-mitigated status hit cache, not Redis."""
# Create mock Redis client
mock_redis, mock_pipeline = self._create_mock_redis()
# Mock the final states check to return non-mitigated
mock_pipeline.execute.return_value = [False, True] # mitigated=False, non_mitigated=True
pov_status = PoVReproduceStatus(mock_redis)
# Create sample request
request = POVReproduceRequest()
request.task_id = "test-task"
request.internal_patch_id = "0"
request.pov_path = "/test/path"
request.sanitizer = "asan"
request.harness_name = "test_harness"
# First call should hit Redis
result1 = pov_status.request_status(request)
assert isinstance(result1, POVReproduceResponse)
assert result1.did_crash is True
# Second call should hit cache, not Redis
result2 = pov_status.request_status(request)
assert isinstance(result2, POVReproduceResponse)
assert result2.did_crash is True
# Redis should only be called once (for the first request)
assert mock_redis.pipeline.call_count == 1
def test_cache_miss_for_pending_status(self):
"""Test that pending status doesn't get cached and always hits Redis."""
# Create mock Redis client
mock_redis, mock_pipeline = self._create_mock_redis()
mock_redis.sadd.return_value = 1 # Mock successful add to pending set
# Mock responses:
# First call: _did_crash returns [False, False], then full check returns [True, False, False]
# Second call: _did_crash hits cache (no Redis call), then full check returns [True, False, False]
mock_pipeline.execute.side_effect = [
[False, False], # First request: _did_crash: not in final states
[
True,
False,
False,
], # First request: request_status full check: pending=True, mitigated=False, non_mitigated=False
[True, False, False], # Second request: request_status full check: still pending (cache hit for _did_crash)
]
pov_status = PoVReproduceStatus(mock_redis)
# Create sample request
request = POVReproduceRequest()
request.task_id = "test-task"
request.internal_patch_id = "0"
request.pov_path = "/test/path"
request.sanitizer = "asan"
request.harness_name = "test_harness"
# First call should return None (pending)
result1 = pov_status.request_status(request)
assert result1 is None
# Second call should also return None (pending) and hit Redis again for full check
# but _did_crash should hit cache
result2 = pov_status.request_status(request)
assert result2 is None
# Redis should be called 3 times total: 1 for first _did_crash + 2 for full checks
# (second _did_crash hits cache)
assert mock_redis.pipeline.call_count == 3
def test_cache_works_with_different_requests(self):
"""Test that cache works correctly for different requests."""
# Create mock Redis client
mock_redis, mock_pipeline = self._create_mock_redis()
# Mock different responses for different requests
mock_pipeline.execute.side_effect = [
[True, False], # request1: mitigated
[False, True], # request2: non-mitigated
# Subsequent calls should hit cache
]
pov_status = PoVReproduceStatus(mock_redis)
# Create two different requests
request1 = POVReproduceRequest()
request1.task_id = "test-task-1"
request1.internal_patch_id = "0"
request1.pov_path = "/test/path1"
request1.sanitizer = "asan"
request1.harness_name = "test_harness"
request2 = POVReproduceRequest()
request2.task_id = "test-task-2"
request2.internal_patch_id = "0"
request2.pov_path = "/test/path2"
request2.sanitizer = "msan"
request2.harness_name = "test_harness"
# First calls should hit Redis
result1 = pov_status.request_status(request1)
assert isinstance(result1, POVReproduceResponse)
assert result1.did_crash is False # mitigated
result2 = pov_status.request_status(request2)
assert isinstance(result2, POVReproduceResponse)
assert result2.did_crash is True # non-mitigated
# Repeat calls should hit cache
result1_cached = pov_status.request_status(request1)
assert isinstance(result1_cached, POVReproduceResponse)
assert result1_cached.did_crash is False
result2_cached = pov_status.request_status(request2)
assert isinstance(result2_cached, POVReproduceResponse)
assert result2_cached.did_crash is True
# Redis should only be called twice (once for each unique request)
assert mock_redis.pipeline.call_count == 2
def test_cache_respects_maxsize(self):
"""Test that cache behavior is consistent (simplified test without maxsize override)."""
# Create mock Redis client
mock_redis, mock_pipeline = self._create_mock_redis()
# Mock all responses as mitigated
mock_pipeline.execute.return_value = [True, False] # mitigated=True, non_mitigated=False
pov_status = PoVReproduceStatus(mock_redis)
# Create multiple different requests
requests = []
for i in range(3):
request = POVReproduceRequest()
request.task_id = f"test-task-{i}"
request.internal_patch_id = "0"
request.pov_path = f"/test/path{i}"
request.sanitizer = "asan"
request.harness_name = "test_harness"
requests.append(request)
# First requests should hit Redis
result1 = pov_status.request_status(requests[0])
result2 = pov_status.request_status(requests[1])
result3 = pov_status.request_status(requests[2])
assert isinstance(result1, POVReproduceResponse)
assert isinstance(result2, POVReproduceResponse)
assert isinstance(result3, POVReproduceResponse)
assert mock_redis.pipeline.call_count == 3
# Repeated requests should hit cache
result1_cached = pov_status.request_status(requests[0])
result2_cached = pov_status.request_status(requests[1])
result3_cached = pov_status.request_status(requests[2])
assert isinstance(result1_cached, POVReproduceResponse)
assert isinstance(result2_cached, POVReproduceResponse)
assert isinstance(result3_cached, POVReproduceResponse)
# Should not have increased - all cache hits
assert mock_redis.pipeline.call_count == 3
def test_separate_instances_have_separate_caches(self):
"""Test that different PoVReproduceStatus instances have separate caches."""
# Create two mock Redis clients
mock_redis1 = MagicMock()
mock_pipeline1 = MagicMock()
mock_redis1.pipeline.return_value = mock_pipeline1
mock_pipeline1.__enter__.return_value = mock_pipeline1
mock_pipeline1.__exit__.return_value = None
mock_pipeline1.execute.return_value = [True, False] # mitigated
mock_redis2 = MagicMock()
mock_pipeline2 = MagicMock()
mock_redis2.pipeline.return_value = mock_pipeline2
mock_pipeline2.__enter__.return_value = mock_pipeline2
mock_pipeline2.__exit__.return_value = None
mock_pipeline2.execute.return_value = [False, True] # non-mitigated
# Create two instances
pov_status1 = PoVReproduceStatus(mock_redis1)
pov_status2 = PoVReproduceStatus(mock_redis2)
# Create same request for both
request = POVReproduceRequest()
request.task_id = "test-task"
request.internal_patch_id = "0"
request.pov_path = "/test/path"
request.sanitizer = "asan"
request.harness_name = "test_harness"
# First calls should hit respective Redis instances
result1 = pov_status1.request_status(request)
result2 = pov_status2.request_status(request)
assert isinstance(result1, POVReproduceResponse)
assert result1.did_crash is False # mitigated
assert isinstance(result2, POVReproduceResponse)
assert result2.did_crash is True # non-mitigated
# Each instance should have hit its Redis once
assert mock_redis1.pipeline.call_count == 1
assert mock_redis2.pipeline.call_count == 1
# Repeat calls should hit respective caches
result1_cached = pov_status1.request_status(request)
result2_cached = pov_status2.request_status(request)
assert isinstance(result1_cached, POVReproduceResponse)
assert result1_cached.did_crash is False
assert isinstance(result2_cached, POVReproduceResponse)
assert result2_cached.did_crash is True
# Redis call counts should not increase (cache hits)
assert mock_redis1.pipeline.call_count == 1
assert mock_redis2.pipeline.call_count == 1
def test_cache_integration_with_real_workflow(self, pov_status, sample_request):
"""Test that caching works correctly in a real workflow with Redis."""
# This test uses real Redis and validates the cache behavior
# Initial request should return None (pending)
result1 = pov_status.request_status(sample_request)
assert result1 is None
# Mark as mitigated
mark_result = pov_status.mark_mitigated(sample_request)
assert mark_result is True
# Now test caching: multiple requests should return same result
# We can't easily test Redis call count with real Redis, but we can
# verify that the behavior is consistent
results = []
for _ in range(5):
result = pov_status.request_status(sample_request)
results.append(result)
# All results should be identical POVReproduceResponse objects
for result in results:
assert isinstance(result, POVReproduceResponse)
assert result.did_crash is False # mitigated
assert result.request.task_id == sample_request.task_id
assert result.request.internal_patch_id == sample_request.internal_patch_id
assert result.request.pov_path == sample_request.pov_path
assert result.request.sanitizer == sample_request.sanitizer
assert result.request.harness_name == sample_request.harness_name
def test_cache_clears_across_different_final_states(self):
"""Test that moving between final states works correctly with caching."""
# Create mock Redis client
mock_redis, mock_pipeline = self._create_mock_redis()
pov_status = PoVReproduceStatus(mock_redis)
# Create sample request
request = POVReproduceRequest()
request.task_id = "test-task"
request.internal_patch_id = "0"
request.pov_path = "/test/path"
request.sanitizer = "asan"
request.harness_name = "test_harness"
# First: mock as mitigated
mock_pipeline.execute.return_value = [True, False]
result1 = pov_status.request_status(request)
assert isinstance(result1, POVReproduceResponse)
assert result1.did_crash is False
# Second call should hit cache
result2 = pov_status.request_status(request)
assert isinstance(result2, POVReproduceResponse)
assert result2.did_crash is False
assert mock_redis.pipeline.call_count == 1 # Only first call hit Redis
# Now simulate the state changing in Redis (e.g., due to another process)
# This would require cache invalidation in a real system, but our current
# implementation doesn't handle this case - the cache will return stale data
# This test documents the current behavior
mock_pipeline.execute.return_value = [False, True] # Now non-mitigated
# This call will still return cached (stale) mitigated result
result3 = pov_status.request_status(request)
assert isinstance(result3, POVReproduceResponse)
assert result3.did_crash is False # Still cached mitigated result
assert mock_redis.pipeline.call_count == 1 # No new Redis calls
@@ -378,7 +378,7 @@ class Scheduler:
vuln_item: RQItem[TracedCrash] | None = self.traced_vulnerabilities_queue.pop()
if vuln_item is not None:
crash: TracedCrash = vuln_item.deserialized
logger.info(f"Submitting vulnerability for task {crash.crash.target.task_id}")
logger.info(f"Recording vulnerability for task {crash.crash.target.task_id}")
if self.submissions.submit_vulnerability(crash):
self.traced_vulnerabilities_queue.ack_item(vuln_item.item_id)
collected_item = True
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff