diff --git a/examples/junit/src/test/java/com/example/BUILD.bazel b/examples/junit/src/test/java/com/example/BUILD.bazel
index a4d119678..09665600b 100644
--- a/examples/junit/src/test/java/com/example/BUILD.bazel
+++ b/examples/junit/src/test/java/com/example/BUILD.bazel
@@ -372,3 +372,24 @@ java_fuzz_target_test(
"@maven//:org_junit_jupiter_junit_jupiter_params",
],
)
+
+# Test for the maximize() hill-climbing API.
+# This test uses Jazzer.maximize() to guide the fuzzer toward maximizing
+# a "temperature" value, demonstrating hill-climbing behavior.
+java_fuzz_target_test(
+ name = "ReactorFuzzTest",
+ srcs = ["ReactorFuzzTest.java"],
+ allowed_findings = ["java.lang.RuntimeException"],
+ env = {"JAZZER_FUZZ": "1"},
+ target_class = "com.example.ReactorFuzzTest",
+ verify_crash_reproducer = False,
+ runtime_deps = [
+ ":junit_runtime",
+ ],
+ deps = [
+ "//src/main/java/com/code_intelligence/jazzer/api:hooks",
+ "//src/main/java/com/code_intelligence/jazzer/junit:fuzz_test",
+ "//src/main/java/com/code_intelligence/jazzer/mutation/annotation",
+ "@maven//:org_junit_jupiter_junit_jupiter_api",
+ ],
+)
diff --git a/examples/junit/src/test/java/com/example/ReactorFuzzTest.java b/examples/junit/src/test/java/com/example/ReactorFuzzTest.java
new file mode 100644
index 000000000..1221d8511
--- /dev/null
+++ b/examples/junit/src/test/java/com/example/ReactorFuzzTest.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2026 Code Intelligence GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.example;
+
+import com.code_intelligence.jazzer.api.Jazzer;
+import com.code_intelligence.jazzer.junit.FuzzTest;
+import com.code_intelligence.jazzer.mutation.annotation.NotNull;
+
+public class ReactorFuzzTest {
+
+ @FuzzTest
+ public void fuzz(@NotNull String input) {
+ for (char c : input.toCharArray()) {
+ if (c < 32 || c > 126) return;
+ }
+ controlReactor(input);
+ }
+
+ private void controlReactor(String commands) {
+ long temperature = 0; // Starts cold
+
+ for (char cmd : commands.toCharArray()) {
+ // Complex, chaotic feedback loop.
+ // It is hard to predict which character increases temperature
+ // because it depends on the CURRENT temperature.
+ if ((temperature ^ cmd) % 3 == 0) {
+ temperature += (cmd % 10); // Heat up slightly
+ } else if ((temperature ^ cmd) % 3 == 1) {
+ temperature -= (cmd % 8); // Cool down slightly
+ } else {
+ temperature += 1; // Tiny increase
+ }
+
+ // Prevent dropping below absolute zero for simulation sanity
+ if (temperature < 0) temperature = 0;
+ }
+ // THE GOAL: MAXIMIZATION
+ // We need to drive 'temperature' to an extreme value.
+ // Standard coverage is 100% constant here (it just loops).
+ Jazzer.maximize(temperature, 500, 4500);
+ if (temperature >= 4500) {
+ throw new RuntimeException("Meltdown! Temperature maximized.");
+ }
+ }
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/api/Jazzer.java b/src/main/java/com/code_intelligence/jazzer/api/Jazzer.java
index 2b882a269..38e34cdcc 100644
--- a/src/main/java/com/code_intelligence/jazzer/api/Jazzer.java
+++ b/src/main/java/com/code_intelligence/jazzer/api/Jazzer.java
@@ -33,6 +33,9 @@ public final class Jazzer {
private static final MethodHandle TRACE_MEMCMP;
private static final MethodHandle TRACE_PC_INDIR;
+ private static final MethodHandle COUNTERS_TRACKER_ALLOCATE;
+ private static final MethodHandle COUNTERS_TRACKER_SET_RANGE;
+
static {
Class> jazzerInternal = null;
MethodHandle onFuzzTargetReady = null;
@@ -40,6 +43,8 @@ public final class Jazzer {
MethodHandle traceStrstr = null;
MethodHandle traceMemcmp = null;
MethodHandle tracePcIndir = null;
+ MethodHandle countersTrackerAllocate = null;
+ MethodHandle countersTrackerSetRange = null;
try {
jazzerInternal = Class.forName("com.code_intelligence.jazzer.runtime.JazzerInternal");
MethodType onFuzzTargetReadyType = MethodType.methodType(void.class, Runnable.class);
@@ -70,6 +75,16 @@ public final class Jazzer {
tracePcIndir =
MethodHandles.publicLookup()
.findStatic(traceDataFlowNativeCallbacks, "tracePcIndir", tracePcIndirType);
+
+ Class> countersTracker =
+ Class.forName("com.code_intelligence.jazzer.runtime.CountersTracker");
+ MethodType allocateType = MethodType.methodType(void.class, int.class, int.class);
+ countersTrackerAllocate =
+ MethodHandles.publicLookup()
+ .findStatic(countersTracker, "ensureCountersAllocated", allocateType);
+ MethodType setRangeType = MethodType.methodType(void.class, int.class, int.class);
+ countersTrackerSetRange =
+ MethodHandles.publicLookup().findStatic(countersTracker, "setCounterRange", setRangeType);
} catch (ClassNotFoundException ignore) {
// Not running in the context of the agent. This is fine as long as no methods are called on
// this class.
@@ -86,6 +101,8 @@ public final class Jazzer {
TRACE_STRSTR = traceStrstr;
TRACE_MEMCMP = traceMemcmp;
TRACE_PC_INDIR = tracePcIndir;
+ COUNTERS_TRACKER_ALLOCATE = countersTrackerAllocate;
+ COUNTERS_TRACKER_SET_RANGE = countersTrackerSetRange;
}
private Jazzer() {}
@@ -93,7 +110,7 @@ private Jazzer() {}
/**
* A 32-bit random number that hooks can use to make pseudo-random choices between multiple
* possible mutations they could guide the fuzzer towards. Hooks must not base the decision
- * whether or not to report a finding on this number as this will make findings non-reproducible.
+ * whether to report a finding on this number as this will make findings non-reproducible.
*
*
This is the same number that libFuzzer uses as a seed internally, which makes it possible to
* deterministically reproduce a previous fuzzing run by supplying the seed value printed by
@@ -230,6 +247,75 @@ public static void exploreState(byte state) {
// an automatically generated call-site id. Without instrumentation, this is a no-op.
}
+ /**
+ * Hill-climbing API to maximize a value. For each observed value v in [minValue, maxValue],
+ * provides feedback that all values in [minValue, v] are covered.
+ *
+ *
This enables corpus minimization to keep only the input resulting in the maximum value.
+ * Values below minValue provide no signal. Values above maxValue are clamped to maxValue.
+ *
+ *
Important: This allocates (maxValue - minValue + 1) coverage counters per unique ID.
+ * For large value ranges, use a mapping function to reduce the range:
+ *
+ *
{@code
+ * // Map [0, 1_000_000] to [0, 1000] steps
+ * long step = value < 0 ? 0 : Math.min(value / 1000, 1000);
+ * Jazzer.maximize(step, id, 0, 1000);
+ * }
+ *
+ * @param value The value to maximize (will be clamped to [minValue, maxValue])
+ * @param id A unique identifier for this call site (must be consistent across runs)
+ * @param minValue The minimum value in the range (inclusive)
+ * @param maxValue The maximum value in the range (inclusive)
+ */
+ public static void maximize(long value, int id, long minValue, long maxValue) {
+ if (maxValue < minValue) {
+ throw new IllegalArgumentException("maxValue must be >= minValue");
+ }
+ long range = maxValue - minValue;
+ if (range < 0 || range > (long) Integer.MAX_VALUE - 1) {
+ throw new IllegalArgumentException(
+ "Range too large: (maxValue - minValue + 1) must be <= Integer.MAX_VALUE");
+ }
+
+ if (COUNTERS_TRACKER_ALLOCATE == null) {
+ return;
+ }
+
+ int numCounters = (int) (range + 1);
+
+ try {
+ // Allocate counters (idempotent, validates numCounters > 0 and consistency)
+ COUNTERS_TRACKER_ALLOCATE.invokeExact(id, numCounters);
+
+ // Set counters if value provides signal
+ if (value >= minValue) {
+ int toOffset = (int) (Math.min(value, maxValue) - minValue);
+ COUNTERS_TRACKER_SET_RANGE.invokeExact(id, toOffset);
+ }
+ } catch (Throwable e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Convenience overload of {@link #maximize(long, int, long, long)} that allows using
+ * automatically generated call-site identifiers. During instrumentation, calls to this method are
+ * replaced with calls to {@link #maximize(long, int, long, long)} using a unique id for each call
+ * site.
+ *
+ * Without instrumentation, this is a no-op.
+ *
+ * @param value The value to maximize
+ * @param minValue The minimum value in the range (inclusive)
+ * @param maxValue The maximum value in the range (inclusive)
+ * @see #maximize(long, int, long, long)
+ */
+ public static void maximize(long value, long minValue, long maxValue) {
+ // Instrumentation replaces calls to this method with calls to maximize(long, int, long, long)
+ // using an automatically generated call-site id. Without instrumentation, this is a no-op.
+ }
+
/**
* Make Jazzer report the provided {@link Throwable} as a finding.
*
diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/BUILD.bazel b/src/main/java/com/code_intelligence/jazzer/runtime/BUILD.bazel
index 2b5f2d0ef..3403dda36 100644
--- a/src/main/java/com/code_intelligence/jazzer/runtime/BUILD.bazel
+++ b/src/main/java/com/code_intelligence/jazzer/runtime/BUILD.bazel
@@ -100,6 +100,22 @@ java_library(
# The following targets must only be referenced directly by tests or native implementations.
+java_jni_library(
+ name = "counters_tracker",
+ srcs = ["CountersTracker.java"],
+ native_libs = select({
+ "@platforms//os:android": ["//src/main/native/com/code_intelligence/jazzer/driver:jazzer_driver"],
+ "//conditions:default": [],
+ }),
+ visibility = [
+ "//src/main/native/com/code_intelligence/jazzer/driver:__pkg__",
+ "//src/test:__subpackages__",
+ ],
+ deps = [
+ "//src/main/java/com/code_intelligence/jazzer/utils:unsafe_provider",
+ ],
+)
+
java_jni_library(
name = "coverage_map",
srcs = ["CoverageMap.java"],
@@ -170,6 +186,7 @@ java_library(
],
deps = [
":constants",
+ ":counters_tracker",
":coverage_map",
":trace_data_flow_native_callbacks",
"//src/main/java/com/code_intelligence/jazzer/api:hooks",
diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/CountersTracker.java b/src/main/java/com/code_intelligence/jazzer/runtime/CountersTracker.java
new file mode 100644
index 000000000..49035b08f
--- /dev/null
+++ b/src/main/java/com/code_intelligence/jazzer/runtime/CountersTracker.java
@@ -0,0 +1,286 @@
+/*
+ * Copyright 2026 Code Intelligence GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.code_intelligence.jazzer.runtime;
+
+import com.code_intelligence.jazzer.utils.UnsafeProvider;
+import com.github.fmeum.rules_jni.RulesJni;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import sun.misc.Unsafe;
+
+/**
+ * Generic foundation for mapping program state to coverage counters.
+ *
+ *
This class provides a flexible API for any consumer wanting to translate program state signals
+ * to coverage counters, enabling incremental progress feedback to the fuzzer. Use cases include:
+ *
+ *
+ * - Hill-climbing (maximize API)
+ *
- State exploration
+ *
- Custom progress signals
+ *
+ *
+ * Each counter is a byte (0-255). Each ID has a range of counters accessible via indexes [0,
+ * numCounters - 1]. Allocation is explicit - call {@link #ensureCountersAllocated} first, then use
+ * the set methods.
+ *
+ *
The counters are allocated from a dedicated memory region separate from the main coverage map,
+ * ensuring isolation and preventing interference with regular coverage tracking.
+ */
+public final class CountersTracker {
+ static {
+ RulesJni.loadLibrary("jazzer_driver", "/com/code_intelligence/jazzer/driver");
+ }
+
+ private static final String ENV_MAX_COUNTERS = "JAZZER_MAXIMIZE_MAX_COUNTERS";
+
+ private static final int DEFAULT_MAX_COUNTERS = 1 << 20;
+
+ /** Maximum number of counters available (default 1M, configurable via environment variable). */
+ private static final int MAX_COUNTERS = initMaxCounters();
+
+ private static final Unsafe UNSAFE = UnsafeProvider.getUnsafe();
+
+ /** Base address of the counter memory region. */
+ private static final long countersAddress = UNSAFE.allocateMemory(MAX_COUNTERS);
+
+ /** Map from ID to allocated counter range. */
+ private static final ConcurrentHashMap idToRange =
+ new ConcurrentHashMap<>();
+
+ /** Next available offset for counter allocation. */
+ private static final AtomicInteger nextOffset = new AtomicInteger(0);
+
+ static {
+ // Zero-initialize the counter region
+ UNSAFE.setMemory(countersAddress, MAX_COUNTERS, (byte) 0);
+ // Initialize native side (like CoverageMap does)
+ initialize(countersAddress);
+ }
+
+ private CountersTracker() {}
+
+ /**
+ * Allocates a range of counters for the given ID.
+ *
+ * Idempotent: if already allocated, validates that numCounters matches.
+ *
+ * @param id Unique identifier for this counter range
+ * @param numCounters Number of counters to allocate
+ * @throws IllegalArgumentException if called with different numCounters for same ID
+ * @throws IllegalStateException if counter space is exhausted
+ */
+ public static void ensureCountersAllocated(int id, int numCounters) {
+ if (numCounters <= 0) {
+ throw new IllegalArgumentException("numCounters must be positive, got: " + numCounters);
+ }
+
+ CounterRange range =
+ idToRange.computeIfAbsent(
+ id,
+ key -> {
+ // Allocate space - only runs once per ID
+ int startOffset;
+ int endOffset;
+ do {
+ startOffset = nextOffset.get();
+ endOffset = startOffset + numCounters;
+ if (endOffset > MAX_COUNTERS) {
+ throw new IllegalStateException(
+ String.format(
+ "Counter space exhausted: requested %d counters at offset %d, "
+ + "but only %d total counters available. "
+ + "Increase via %s environment variable or use smaller ranges.",
+ numCounters, startOffset, MAX_COUNTERS, ENV_MAX_COUNTERS));
+ }
+ } while (!nextOffset.compareAndSet(startOffset, endOffset));
+
+ CounterRange newRange = new CounterRange(startOffset, numCounters);
+
+ // Register the new counters with libFuzzer
+ registerCounters(startOffset, endOffset);
+
+ return newRange;
+ });
+
+ // Validate numCounters matches (for calls with same ID but different numCounters)
+ if (range.numCounters != numCounters) {
+ throw new IllegalArgumentException(
+ String.format(
+ "ensureCountersAllocated() called with different numCounters for id %d: "
+ + "existing=%d, requested=%d",
+ id, range.numCounters, numCounters));
+ }
+ }
+
+ /**
+ * Helper to get range for an allocated ID, throws if not allocated.
+ *
+ * @param id The ID to look up
+ * @return The CounterRange for this ID
+ * @throws IllegalStateException if no counters allocated for this ID
+ */
+ private static CounterRange getRange(int id) {
+ CounterRange range = idToRange.get(id);
+ if (range == null) {
+ throw new IllegalStateException("No counters allocated for id: " + id);
+ }
+ return range;
+ }
+
+ /**
+ * Sets the value of a specific counter within a range.
+ *
+ * @param id The ID of the allocated counter range
+ * @param offset Offset within the range [0, numCounters)
+ * @param value The value to set (0-255)
+ * @throws IllegalStateException if no counters allocated for this ID
+ * @throws IndexOutOfBoundsException if offset is out of bounds
+ */
+ public static void setCounter(int id, int offset, byte value) {
+ CounterRange range = getRange(id);
+ if (offset < 0 || offset >= range.numCounters) {
+ throw new IndexOutOfBoundsException(
+ String.format(
+ "Counter offset %d out of bounds for range with %d counters",
+ offset, range.numCounters));
+ }
+ long address = countersAddress + range.startOffset + offset;
+ UNSAFE.putByte(address, value);
+ }
+
+ /**
+ * Sets the first counter (offset = 0) to the given value.
+ *
+ * @param id The ID of the allocated counter range
+ * @param value The value to set (0-255)
+ * @throws IllegalStateException if no counters allocated for this ID
+ */
+ public static void setCounter(int id, byte value) {
+ setCounter(id, 0, value);
+ }
+
+ /**
+ * Sets the first counter (offset = 0) to 1.
+ *
+ * @param id The ID of the allocated counter range
+ * @throws IllegalStateException if no counters allocated for this ID
+ */
+ public static void setCounter(int id) {
+ setCounter(id, 0, (byte) 1);
+ }
+
+ /**
+ * Sets multiple consecutive counters to a value.
+ *
+ *
Efficient for setting ranges (e.g., all counters from 0 to N for hill-climbing).
+ *
+ * @param id The ID of the allocated counter range
+ * @param fromOffset Start offset (inclusive)
+ * @param toOffset End offset (inclusive)
+ * @param value The value to set
+ * @throws IllegalStateException if no counters allocated for this ID
+ * @throws IndexOutOfBoundsException if offsets are out of bounds
+ */
+ public static void setCounterRange(int id, int fromOffset, int toOffset, byte value) {
+ CounterRange range = getRange(id);
+ if (fromOffset < 0) {
+ throw new IndexOutOfBoundsException("fromOffset must be non-negative, got: " + fromOffset);
+ }
+ if (toOffset >= range.numCounters) {
+ throw new IndexOutOfBoundsException(
+ String.format(
+ "toOffset %d out of bounds for range with %d counters", toOffset, range.numCounters));
+ }
+ if (fromOffset > toOffset) {
+ throw new IllegalArgumentException(
+ String.format(
+ "fromOffset (%d) must not be greater than toOffset (%d)", fromOffset, toOffset));
+ }
+
+ long startAddress = countersAddress + range.startOffset + fromOffset;
+ int length = toOffset - fromOffset + 1;
+ UNSAFE.setMemory(startAddress, length, value);
+ }
+
+ /**
+ * Sets counters from offset 0 to toOffset (inclusive) to the given value.
+ *
+ * @param id The ID of the allocated counter range
+ * @param toOffset End offset (inclusive)
+ * @param value The value to set
+ * @throws IllegalStateException if no counters allocated for this ID
+ * @throws IndexOutOfBoundsException if toOffset is out of bounds
+ */
+ public static void setCounterRange(int id, int toOffset, byte value) {
+ setCounterRange(id, 0, toOffset, value);
+ }
+
+ /**
+ * Sets counters from offset 0 to toOffset (inclusive) to 1.
+ *
+ *
Ideal for hill-climbing/maximize patterns where you want to signal progress up to a point.
+ *
+ * @param id The ID of the allocated counter range
+ * @param toOffset End offset (inclusive)
+ * @throws IllegalStateException if no counters allocated for this ID
+ * @throws IndexOutOfBoundsException if toOffset is out of bounds
+ */
+ public static void setCounterRange(int id, int toOffset) {
+ setCounterRange(id, 0, toOffset, (byte) 1);
+ }
+
+ /** Internal record of an allocated counter range. */
+ private static final class CounterRange {
+ final int startOffset;
+ final int numCounters;
+
+ CounterRange(int startOffset, int numCounters) {
+ this.startOffset = startOffset;
+ this.numCounters = numCounters;
+ }
+ }
+
+ private static int initMaxCounters() {
+ String value = System.getenv(ENV_MAX_COUNTERS);
+ if (value == null || value.isEmpty()) {
+ return DEFAULT_MAX_COUNTERS;
+ }
+ try {
+ return Integer.parseInt(value.trim());
+ } catch (NumberFormatException e) {
+ return DEFAULT_MAX_COUNTERS;
+ }
+ }
+
+ // Native methods
+
+ /**
+ * Initializes the native counter tracker with the base address of the counter region.
+ *
+ * @param countersAddress The base address of the counter memory region
+ */
+ private static native void initialize(long countersAddress);
+
+ /**
+ * Registers a range of counters with libFuzzer.
+ *
+ * @param startOffset Start offset of the range to register
+ * @param endOffset End offset (exclusive) of the range to register
+ */
+ private static native void registerCounters(int startOffset, int endOffset);
+}
diff --git a/src/main/java/com/code_intelligence/jazzer/runtime/JazzerApiHooks.java b/src/main/java/com/code_intelligence/jazzer/runtime/JazzerApiHooks.java
index d1d9c1c01..b16790d26 100644
--- a/src/main/java/com/code_intelligence/jazzer/runtime/JazzerApiHooks.java
+++ b/src/main/java/com/code_intelligence/jazzer/runtime/JazzerApiHooks.java
@@ -43,4 +43,21 @@ public static void exploreStateWithId(
MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
Jazzer.exploreState((byte) arguments[0], hookId);
}
+
+ /**
+ * Replaces calls to {@link Jazzer#maximize(long, long, long)} with calls to {@link
+ * Jazzer#maximize(long, int, long, long)} using the hook id as the id parameter.
+ *
+ *
This allows each call site to be tracked separately without requiring the user to manually
+ * provide a unique id.
+ */
+ @MethodHook(
+ type = HookType.REPLACE,
+ targetClassName = "com.code_intelligence.jazzer.api.Jazzer",
+ targetMethod = "maximize",
+ targetMethodDescriptor = "(JJJ)V")
+ public static void maximizeWithId(
+ MethodHandle method, Object thisObject, Object[] arguments, int hookId) {
+ Jazzer.maximize((long) arguments[0], hookId, (long) arguments[1], (long) arguments[2]);
+ }
}
diff --git a/src/main/native/com/code_intelligence/jazzer/driver/BUILD.bazel b/src/main/native/com/code_intelligence/jazzer/driver/BUILD.bazel
index 4f2deef0e..ed7c5400a 100644
--- a/src/main/native/com/code_intelligence/jazzer/driver/BUILD.bazel
+++ b/src/main/native/com/code_intelligence/jazzer/driver/BUILD.bazel
@@ -25,7 +25,7 @@ cc_library(
name = "jazzer_driver_lib",
visibility = ["//src/test/native/com/code_intelligence/jazzer/driver/mocks:__pkg__"],
deps = [
- ":coverage_tracker",
+ ":counters_tracker",
":fuzz_target_runner",
":jazzer_fuzzer_callbacks",
":libfuzzer_callbacks",
@@ -45,10 +45,13 @@ cc_jni_library(
)
cc_library(
- name = "coverage_tracker",
- srcs = ["coverage_tracker.cpp"],
- hdrs = ["coverage_tracker.h"],
- deps = ["//src/main/java/com/code_intelligence/jazzer/runtime:coverage_map.hdrs"],
+ name = "counters_tracker",
+ srcs = ["counters_tracker.cpp"],
+ hdrs = ["counters_tracker.h"],
+ deps = [
+ "//src/main/java/com/code_intelligence/jazzer/runtime:counters_tracker.hdrs",
+ "//src/main/java/com/code_intelligence/jazzer/runtime:coverage_map.hdrs",
+ ],
# Symbols are only referenced dynamically via JNI.
alwayslink = True,
)
diff --git a/src/main/native/com/code_intelligence/jazzer/driver/counters_tracker.cpp b/src/main/native/com/code_intelligence/jazzer/driver/counters_tracker.cpp
new file mode 100644
index 000000000..ce556fd60
--- /dev/null
+++ b/src/main/native/com/code_intelligence/jazzer/driver/counters_tracker.cpp
@@ -0,0 +1,178 @@
+// Copyright 2024 Code Intelligence GmbH
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "counters_tracker.h"
+
+#include
+#include
+
+#include
+#include
+
+#include "com_code_intelligence_jazzer_runtime_CountersTracker.h"
+#include "com_code_intelligence_jazzer_runtime_CoverageMap.h"
+
+extern "C" void __sanitizer_cov_8bit_counters_init(uint8_t *start,
+ uint8_t *end);
+extern "C" void __sanitizer_cov_pcs_init(const uintptr_t *pcs_beg,
+ const uintptr_t *pcs_end);
+extern "C" size_t __sanitizer_cov_get_observed_pcs(uintptr_t **pc_entries);
+
+namespace {
+void AssertNoException(JNIEnv &env) {
+ if (env.ExceptionCheck()) {
+ env.ExceptionDescribe();
+ std::cerr << "ERROR: Java exception occurred in CountersTracker JNI code"
+ << std::endl;
+ _Exit(1);
+ }
+}
+} // namespace
+
+namespace jazzer {
+
+uint8_t *CountersTracker::coverage_counters_ = nullptr;
+uint8_t *CountersTracker::extra_counters_ = nullptr;
+std::mutex CountersTracker::mutex_;
+
+void CountersTracker::RegisterCounterRange(uint8_t *start, uint8_t *end) {
+ if (start >= end) {
+ return;
+ }
+
+ std::size_t num_counters = end - start;
+
+ // libFuzzer requires an array containing the instruction addresses associated
+ // with the coverage counters. Since these may be synthetic counters (not
+ // associated with real code), we create PC entries with the flag set to 0 to
+ // indicate they are not real PCs. The PC value is set to the counter index
+ // for identification purposes.
+ PCTableEntry *pc_entries = new PCTableEntry[num_counters];
+ for (std::size_t i = 0; i < num_counters; ++i) {
+ pc_entries[i] = {i, 0};
+ }
+
+ std::lock_guard lock(mutex_);
+ __sanitizer_cov_8bit_counters_init(start, end);
+ __sanitizer_cov_pcs_init(
+ reinterpret_cast(pc_entries),
+ reinterpret_cast(pc_entries + num_counters));
+}
+
+void CountersTracker::Initialize(JNIEnv &env, jlong counters) {
+ if (coverage_counters_ != nullptr) {
+ std::cerr << "ERROR: CountersTracker::Initialize must not be called more "
+ "than once"
+ << std::endl;
+ _Exit(1);
+ }
+ coverage_counters_ =
+ reinterpret_cast(static_cast(counters));
+}
+
+void CountersTracker::RegisterNewCounters(JNIEnv &env, jint old_num_counters,
+ jint new_num_counters) {
+ if (coverage_counters_ == nullptr) {
+ std::cerr
+ << "ERROR: CountersTracker::Initialize should have been called first"
+ << std::endl;
+ _Exit(1);
+ }
+ if (new_num_counters < old_num_counters) {
+ std::cerr
+ << "ERROR: new_num_counters must not be smaller than old_num_counters"
+ << std::endl;
+ _Exit(1);
+ }
+ RegisterCounterRange(coverage_counters_ + old_num_counters,
+ coverage_counters_ + new_num_counters);
+}
+
+void CountersTracker::InitializeExtra(JNIEnv &env, jlong counters) {
+ if (extra_counters_ != nullptr) {
+ std::cerr
+ << "ERROR: CountersTracker::InitializeExtra must not be called more "
+ "than once"
+ << std::endl;
+ _Exit(1);
+ }
+ extra_counters_ =
+ reinterpret_cast(static_cast(counters));
+}
+
+void CountersTracker::RegisterExtraCounters(JNIEnv &env, jint start_offset,
+ jint end_offset) {
+ if (extra_counters_ == nullptr) {
+ std::cerr << "ERROR: CountersTracker::InitializeExtra should have been "
+ "called first"
+ << std::endl;
+ _Exit(1);
+ }
+ if (end_offset < start_offset) {
+ std::cerr << "ERROR: end_offset must not be smaller than start_offset"
+ << std::endl;
+ _Exit(1);
+ }
+ RegisterCounterRange(extra_counters_ + start_offset,
+ extra_counters_ + end_offset);
+}
+
+} // namespace jazzer
+
+// JNI exports for CoverageMap
+
+[[maybe_unused]] void
+Java_com_code_1intelligence_jazzer_runtime_CoverageMap_initialize(
+ JNIEnv *env, jclass, jlong counters) {
+ ::jazzer::CountersTracker::Initialize(*env, counters);
+}
+
+[[maybe_unused]] void
+Java_com_code_1intelligence_jazzer_runtime_CoverageMap_registerNewCounters(
+ JNIEnv *env, jclass, jint old_num_counters, jint new_num_counters) {
+ ::jazzer::CountersTracker::RegisterNewCounters(*env, old_num_counters,
+ new_num_counters);
+}
+
+[[maybe_unused]] jintArray
+Java_com_code_1intelligence_jazzer_runtime_CoverageMap_getEverCoveredIds(
+ JNIEnv *env, jclass) {
+ uintptr_t *covered_pcs;
+ jint num_covered_pcs = __sanitizer_cov_get_observed_pcs(&covered_pcs);
+ std::vector covered_edge_ids(covered_pcs,
+ covered_pcs + num_covered_pcs);
+ delete[] covered_pcs;
+
+ jintArray covered_edge_ids_jni = env->NewIntArray(num_covered_pcs);
+ AssertNoException(*env);
+ env->SetIntArrayRegion(covered_edge_ids_jni, 0, num_covered_pcs,
+ covered_edge_ids.data());
+ AssertNoException(*env);
+ return covered_edge_ids_jni;
+}
+
+// JNI exports for CountersTracker
+
+[[maybe_unused]] void
+Java_com_code_1intelligence_jazzer_runtime_CountersTracker_initialize(
+ JNIEnv *env, jclass, jlong counters) {
+ ::jazzer::CountersTracker::InitializeExtra(*env, counters);
+}
+
+[[maybe_unused]] void
+Java_com_code_1intelligence_jazzer_runtime_CountersTracker_registerCounters(
+ JNIEnv *env, jclass, jint start_offset, jint end_offset) {
+ ::jazzer::CountersTracker::RegisterExtraCounters(*env, start_offset,
+ end_offset);
+}
diff --git a/src/main/native/com/code_intelligence/jazzer/driver/coverage_tracker.h b/src/main/native/com/code_intelligence/jazzer/driver/counters_tracker.h
similarity index 50%
rename from src/main/native/com/code_intelligence/jazzer/driver/coverage_tracker.h
rename to src/main/native/com/code_intelligence/jazzer/driver/counters_tracker.h
index 34631af8c..ba7a15eb0 100644
--- a/src/main/native/com/code_intelligence/jazzer/driver/coverage_tracker.h
+++ b/src/main/native/com/code_intelligence/jazzer/driver/counters_tracker.h
@@ -17,7 +17,7 @@
#include
#include
-#include
+#include
namespace jazzer {
@@ -26,16 +26,34 @@ struct __attribute__((packed)) PCTableEntry {
[[maybe_unused]] uintptr_t PC, PCFlags;
};
-// CoverageTracker registers an array of 8-bit coverage counters with
-// libFuzzer. The array is populated from Java using Unsafe.
-class CoverageTracker {
+// CountersTracker manages coverage counter arrays and registers them with
+// libFuzzer. It handles two separate counter regions:
+// - Coverage counters: for bytecode edge coverage (used by CoverageMap)
+// - Extra counters: for user APIs like maximize() (used by
+// CountersTracker.java)
+class CountersTracker {
private:
- static uint8_t *counters_;
- static PCTableEntry *pc_entries_;
+ static uint8_t *coverage_counters_;
+ static uint8_t *extra_counters_;
+ static std::mutex mutex_;
+
+ // Shared helper to register a counter range with libFuzzer.
+ static void RegisterCounterRange(uint8_t *start, uint8_t *end);
public:
+ // For CoverageMap: initialize coverage counters base address.
static void Initialize(JNIEnv &env, jlong counters);
+
+ // For CoverageMap: register new coverage counters with libFuzzer.
static void RegisterNewCounters(JNIEnv &env, jint old_num_counters,
jint new_num_counters);
+
+ // For CountersTracker.java: initialize extra counters base address.
+ static void InitializeExtra(JNIEnv &env, jlong counters);
+
+ // For CountersTracker.java: register extra counters with libFuzzer.
+ static void RegisterExtraCounters(JNIEnv &env, jint start_offset,
+ jint end_offset);
};
+
} // namespace jazzer
diff --git a/src/main/native/com/code_intelligence/jazzer/driver/coverage_tracker.cpp b/src/main/native/com/code_intelligence/jazzer/driver/coverage_tracker.cpp
deleted file mode 100644
index 58a05f1a7..000000000
--- a/src/main/native/com/code_intelligence/jazzer/driver/coverage_tracker.cpp
+++ /dev/null
@@ -1,122 +0,0 @@
-// Copyright 2024 Code Intelligence GmbH
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-#include "coverage_tracker.h"
-
-#include
-#include
-
-#include
-#include
-
-#include "com_code_intelligence_jazzer_runtime_CoverageMap.h"
-
-extern "C" void __sanitizer_cov_8bit_counters_init(uint8_t *start,
- uint8_t *end);
-extern "C" void __sanitizer_cov_pcs_init(const uintptr_t *pcs_beg,
- const uintptr_t *pcs_end);
-extern "C" size_t __sanitizer_cov_get_observed_pcs(uintptr_t **pc_entries);
-
-namespace {
-void AssertNoException(JNIEnv &env) {
- if (env.ExceptionCheck()) {
- env.ExceptionDescribe();
- std::cerr << "ERROR: Java exception occurred in CoverageTracker JNI code"
- << std::endl;
- _Exit(1);
- }
-}
-} // namespace
-
-namespace jazzer {
-
-uint8_t *CoverageTracker::counters_ = nullptr;
-PCTableEntry *CoverageTracker::pc_entries_ = nullptr;
-
-void CoverageTracker::Initialize(JNIEnv &env, jlong counters) {
- if (counters_ != nullptr) {
- std::cerr << "ERROR: CoverageTracker::Initialize must not be called more "
- "than once"
- << std::endl;
- _Exit(1);
- }
- counters_ = reinterpret_cast(static_cast(counters));
-}
-
-void CoverageTracker::RegisterNewCounters(JNIEnv &env, jint old_num_counters,
- jint new_num_counters) {
- if (counters_ == nullptr) {
- std::cerr
- << "ERROR: CoverageTracker::Initialize should have been called first"
- << std::endl;
- _Exit(1);
- }
- if (new_num_counters < old_num_counters) {
- std::cerr
- << "ERROR: new_num_counters must not be smaller than old_num_counters"
- << std::endl;
- _Exit(1);
- }
- if (new_num_counters == old_num_counters) {
- return;
- }
- std::size_t diff_num_counters = new_num_counters - old_num_counters;
- // libFuzzer requires an array containing the instruction addresses associated
- // with the coverage counters registered above. This is required to report how
- // many edges have been covered. However, libFuzzer only checks these
- // addresses when the corresponding flag is set to 1. Therefore, it is safe to
- // set the all PC entries to any value as long as the corresponding flag is
- // set to zero. We set the value of each PC to the index of the corresponding
- // edge ID. This facilitates finding the edge ID of each covered PC reported
- // by libFuzzer.
- pc_entries_ = new PCTableEntry[diff_num_counters];
- for (std::size_t i = 0; i < diff_num_counters; ++i) {
- pc_entries_[i] = {i, 0};
- }
- __sanitizer_cov_8bit_counters_init(counters_ + old_num_counters,
- counters_ + new_num_counters);
- __sanitizer_cov_pcs_init((uintptr_t *)(pc_entries_),
- (uintptr_t *)(pc_entries_ + diff_num_counters));
-}
-} // namespace jazzer
-
-[[maybe_unused]] void
-Java_com_code_1intelligence_jazzer_runtime_CoverageMap_initialize(
- JNIEnv *env, jclass, jlong counters) {
- ::jazzer::CoverageTracker::Initialize(*env, counters);
-}
-
-[[maybe_unused]] void
-Java_com_code_1intelligence_jazzer_runtime_CoverageMap_registerNewCounters(
- JNIEnv *env, jclass, jint old_num_counters, jint new_num_counters) {
- ::jazzer::CoverageTracker::RegisterNewCounters(*env, old_num_counters,
- new_num_counters);
-}
-
-[[maybe_unused]] jintArray
-Java_com_code_1intelligence_jazzer_runtime_CoverageMap_getEverCoveredIds(
- JNIEnv *env, jclass) {
- uintptr_t *covered_pcs;
- jint num_covered_pcs = __sanitizer_cov_get_observed_pcs(&covered_pcs);
- std::vector covered_edge_ids(covered_pcs,
- covered_pcs + num_covered_pcs);
- delete[] covered_pcs;
-
- jintArray covered_edge_ids_jni = env->NewIntArray(num_covered_pcs);
- AssertNoException(*env);
- env->SetIntArrayRegion(covered_edge_ids_jni, 0, num_covered_pcs,
- covered_edge_ids.data());
- AssertNoException(*env);
- return covered_edge_ids_jni;
-}
diff --git a/src/test/java/com/code_intelligence/jazzer/api/BUILD.bazel b/src/test/java/com/code_intelligence/jazzer/api/BUILD.bazel
index 86014c7bd..8d0c0be51 100644
--- a/src/test/java/com/code_intelligence/jazzer/api/BUILD.bazel
+++ b/src/test/java/com/code_intelligence/jazzer/api/BUILD.bazel
@@ -1,3 +1,5 @@
+load("//bazel:compat.bzl", "SKIP_ON_WINDOWS")
+
java_test(
name = "AutofuzzTest",
size = "small",
@@ -20,3 +22,22 @@ java_test(
"@maven//:junit_junit",
],
)
+
+java_test(
+ name = "MaximizeTest",
+ size = "small",
+ srcs = [
+ "MaximizeTest.java",
+ ],
+ target_compatible_with = SKIP_ON_WINDOWS,
+ test_class = "com.code_intelligence.jazzer.api.MaximizeTest",
+ runtime_deps = [
+ # Needed for CountersTracker at runtime.
+ "//src/main/java/com/code_intelligence/jazzer/runtime:counters_tracker",
+ ],
+ deps = [
+ "//src/main/java/com/code_intelligence/jazzer/api:hooks",
+ "//src/main/native/com/code_intelligence/jazzer/driver:jazzer_driver",
+ "@maven//:junit_junit",
+ ],
+)
diff --git a/src/test/java/com/code_intelligence/jazzer/api/MaximizeTest.java b/src/test/java/com/code_intelligence/jazzer/api/MaximizeTest.java
new file mode 100644
index 000000000..a99173abb
--- /dev/null
+++ b/src/test/java/com/code_intelligence/jazzer/api/MaximizeTest.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2026 Code Intelligence GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.code_intelligence.jazzer.api;
+
+import org.junit.Test;
+
+public class MaximizeTest {
+
+ @Test
+ public void testMaximizeBasic() {
+ // Basic usage - should not throw
+ Jazzer.maximize(50, 5000, 0, 100);
+ }
+
+ @Test
+ public void testMaximizeWithMinValue() {
+ // Value at minimum
+ Jazzer.maximize(0, 5001, 0, 100);
+ }
+
+ @Test
+ public void testMaximizeWithMaxValue() {
+ // Value at maximum
+ Jazzer.maximize(100, 5002, 0, 100);
+ }
+
+ @Test
+ public void testMaximizeBelowMinValue() {
+ // Value below minimum - should be a no-op (no signal)
+ Jazzer.maximize(-10, 5003, 0, 100);
+ }
+
+ @Test
+ public void testMaximizeAboveMaxValue() {
+ // Value above maximum - should be clamped
+ Jazzer.maximize(200, 5004, 0, 100);
+ }
+
+ @Test
+ public void testMaximizeNegativeRange() {
+ // Negative range
+ Jazzer.maximize(-50, 5005, -100, 0);
+ }
+
+ @Test
+ public void testMaximizeSingleValueRange() {
+ // Range with single value
+ Jazzer.maximize(42, 5006, 42, 42);
+ }
+
+ @Test
+ public void testMaximizeInvalidRange() {
+ // maxValue < minValue - without runtime this is a no-op,
+ // with runtime CountersTracker catches "numCounters must be positive"
+ Jazzer.maximize(50, 5007, 100, 0);
+ }
+
+ @Test
+ public void testMaximizeSameRangeSucceeds() {
+ // Multiple calls with same id and range should succeed
+ Jazzer.maximize(25, 5009, 0, 100);
+ Jazzer.maximize(50, 5009, 0, 100);
+ Jazzer.maximize(75, 5009, 0, 100);
+ }
+
+ @Test
+ public void testMaximizeLargeRange() {
+ // Extremely large range - without runtime this is a no-op,
+ // with runtime would fail in CountersTracker
+ Jazzer.maximize(0, 5010, Long.MIN_VALUE, Long.MAX_VALUE);
+ }
+
+ @Test
+ public void testMaximizeDifferentIds() {
+ // Different IDs can have different ranges
+ Jazzer.maximize(50, 5011, 0, 100);
+ Jazzer.maximize(500, 5012, 0, 1000);
+ Jazzer.maximize(-5, 5013, -10, 10);
+ }
+
+ @Test
+ public void testMaximizeNoOpOverload() {
+ // The overload without explicit ID is a no-op without instrumentation
+ // This should not throw
+ Jazzer.maximize(50, 0, 100);
+ }
+}
diff --git a/src/test/java/com/code_intelligence/jazzer/runtime/BUILD.bazel b/src/test/java/com/code_intelligence/jazzer/runtime/BUILD.bazel
index db8e507a9..8907926ff 100644
--- a/src/test/java/com/code_intelligence/jazzer/runtime/BUILD.bazel
+++ b/src/test/java/com/code_intelligence/jazzer/runtime/BUILD.bazel
@@ -1,5 +1,18 @@
load("//bazel:compat.bzl", "SKIP_ON_WINDOWS")
+java_test(
+ name = "CountersTrackerTest",
+ srcs = [
+ "CountersTrackerTest.java",
+ ],
+ target_compatible_with = SKIP_ON_WINDOWS,
+ deps = [
+ "//src/main/java/com/code_intelligence/jazzer/runtime:counters_tracker",
+ "//src/main/native/com/code_intelligence/jazzer/driver:jazzer_driver",
+ "@maven//:junit_junit",
+ ],
+)
+
java_test(
name = "TraceCmpHooksTest",
srcs = [
diff --git a/src/test/java/com/code_intelligence/jazzer/runtime/CountersTrackerTest.java b/src/test/java/com/code_intelligence/jazzer/runtime/CountersTrackerTest.java
new file mode 100644
index 000000000..28ae5b506
--- /dev/null
+++ b/src/test/java/com/code_intelligence/jazzer/runtime/CountersTrackerTest.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright 2026 Code Intelligence GmbH
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.code_intelligence.jazzer.runtime;
+
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import org.junit.Test;
+
+public class CountersTrackerTest {
+
+ @Test
+ public void testEnsureCountersAllocated() {
+ // Use unique ID to avoid overlap with other tests
+ CountersTracker.ensureCountersAllocated(1000, 100);
+
+ // Should not throw - idempotent with same numCounters
+ CountersTracker.ensureCountersAllocated(1000, 100);
+ }
+
+ @Test
+ public void testEnsureCountersAllocatedDifferentNumCountersThrows() {
+ CountersTracker.ensureCountersAllocated(1001, 100);
+
+ try {
+ CountersTracker.ensureCountersAllocated(1001, 200);
+ fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ assertTrue(e.getMessage().contains("different numCounters"));
+ }
+ }
+
+ @Test
+ public void testEnsureCountersAllocatedInvalidNumCountersThrows() {
+ try {
+ CountersTracker.ensureCountersAllocated(1002, 0);
+ fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ assertTrue(e.getMessage().contains("must be positive"));
+ }
+
+ try {
+ CountersTracker.ensureCountersAllocated(1003, -1);
+ fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ assertTrue(e.getMessage().contains("must be positive"));
+ }
+ }
+
+ @Test
+ public void testSetCounterFullForm() {
+ CountersTracker.ensureCountersAllocated(1004, 10);
+
+ // Should not throw
+ CountersTracker.setCounter(1004, 0, (byte) 1);
+ CountersTracker.setCounter(1004, 5, (byte) 42);
+ CountersTracker.setCounter(1004, 9, (byte) 255);
+ }
+
+ @Test
+ public void testSetCounterConvenienceOverloads() {
+ CountersTracker.ensureCountersAllocated(1005, 10);
+
+ // setCounter(id, value) - offset = 0
+ CountersTracker.setCounter(1005, (byte) 42);
+
+ // setCounter(id) - offset = 0, value = 1
+ CountersTracker.setCounter(1005);
+ }
+
+ @Test
+ public void testSetCounterNotAllocatedThrows() {
+ try {
+ CountersTracker.setCounter(9999999, 0, (byte) 1);
+ fail("Expected IllegalStateException");
+ } catch (IllegalStateException e) {
+ assertTrue(e.getMessage().contains("No counters allocated"));
+ }
+ }
+
+ @Test
+ public void testSetCounterOutOfBoundsThrows() {
+ CountersTracker.ensureCountersAllocated(1006, 10);
+
+ try {
+ CountersTracker.setCounter(1006, -1, (byte) 1);
+ fail("Expected IndexOutOfBoundsException");
+ } catch (IndexOutOfBoundsException e) {
+ // Expected
+ }
+
+ try {
+ CountersTracker.setCounter(1006, 10, (byte) 1);
+ fail("Expected IndexOutOfBoundsException");
+ } catch (IndexOutOfBoundsException e) {
+ // Expected
+ }
+ }
+
+ @Test
+ public void testSetCounterRangeFullForm() {
+ CountersTracker.ensureCountersAllocated(1007, 100);
+
+ // Should not throw
+ CountersTracker.setCounterRange(1007, 0, 50, (byte) 1);
+ CountersTracker.setCounterRange(1007, 0, 99, (byte) 1);
+ CountersTracker.setCounterRange(1007, 50, 50, (byte) 1);
+ }
+
+ @Test
+ public void testSetCounterRangeConvenienceOverloads() {
+ CountersTracker.ensureCountersAllocated(1008, 100);
+
+ // setCounterRange(id, toOffset, value) - fromOffset = 0
+ CountersTracker.setCounterRange(1008, 50, (byte) 42);
+
+ // setCounterRange(id, toOffset) - fromOffset = 0, value = 1
+ CountersTracker.setCounterRange(1008, 99);
+ }
+
+ @Test
+ public void testSetCounterRangeEmptyThrows() {
+ CountersTracker.ensureCountersAllocated(1009, 100);
+
+ try {
+ // Empty range (fromOffset > toOffset) should throw
+ CountersTracker.setCounterRange(1009, 50, 40, (byte) 1);
+ fail("Expected IllegalArgumentException");
+ } catch (IllegalArgumentException e) {
+ assertTrue(e.getMessage().contains("must not be greater than"));
+ }
+ }
+
+ @Test
+ public void testSetCounterRangeNotAllocatedThrows() {
+ try {
+ CountersTracker.setCounterRange(9999998, 0, 5, (byte) 1);
+ fail("Expected IllegalStateException");
+ } catch (IllegalStateException e) {
+ assertTrue(e.getMessage().contains("No counters allocated"));
+ }
+ }
+
+ @Test
+ public void testSetCounterRangeOutOfBoundsThrows() {
+ CountersTracker.ensureCountersAllocated(1010, 10);
+
+ try {
+ CountersTracker.setCounterRange(1010, -1, 5, (byte) 1);
+ fail("Expected IndexOutOfBoundsException");
+ } catch (IndexOutOfBoundsException e) {
+ assertTrue(e.getMessage().contains("non-negative"));
+ }
+
+ try {
+ CountersTracker.setCounterRange(1010, 0, 10, (byte) 1);
+ fail("Expected IndexOutOfBoundsException");
+ } catch (IndexOutOfBoundsException e) {
+ assertTrue(e.getMessage().contains("out of bounds"));
+ }
+ }
+
+ @Test
+ public void testConcurrentAllocation() throws InterruptedException {
+ final int numThreads = 10;
+ final int numAllocationsPerThread = 100;
+ final ExecutorService executor = Executors.newFixedThreadPool(numThreads);
+ final CountDownLatch startLatch = new CountDownLatch(1);
+ final CountDownLatch doneLatch = new CountDownLatch(numThreads);
+ final AtomicReference error = new AtomicReference<>();
+
+ for (int t = 0; t < numThreads; t++) {
+ final int threadId = t;
+ executor.submit(
+ () -> {
+ try {
+ startLatch.await();
+ for (int i = 0; i < numAllocationsPerThread; i++) {
+ // Use a large base ID to avoid overlap with other tests
+ int id = 100000 + threadId * 200 + i;
+ CountersTracker.ensureCountersAllocated(id, 10);
+ // Also test that we can use the counters after allocation
+ CountersTracker.setCounter(id, 0, (byte) 1);
+ }
+ } catch (Throwable e) {
+ error.compareAndSet(null, e);
+ } finally {
+ doneLatch.countDown();
+ }
+ });
+ }
+
+ // Start all threads at once
+ startLatch.countDown();
+
+ // Wait for completion
+ assertTrue("Threads didn't finish in time", doneLatch.await(30, TimeUnit.SECONDS));
+ executor.shutdown();
+
+ if (error.get() != null) {
+ fail("Concurrent allocation failed: " + error.get().getMessage());
+ }
+ }
+
+ @Test
+ public void testConcurrentAllocationSameId() throws InterruptedException {
+ final int numThreads = 10;
+ final int sharedId = 200000; // Use unique ID to avoid overlap with other tests
+ final int numCounters = 50;
+ final ExecutorService executor = Executors.newFixedThreadPool(numThreads);
+ final CountDownLatch startLatch = new CountDownLatch(1);
+ final CountDownLatch doneLatch = new CountDownLatch(numThreads);
+ final AtomicReference error = new AtomicReference<>();
+
+ for (int t = 0; t < numThreads; t++) {
+ executor.submit(
+ () -> {
+ try {
+ startLatch.await();
+ // All threads try to allocate the same ID - should be idempotent
+ CountersTracker.ensureCountersAllocated(sharedId, numCounters);
+ // All threads should be able to use the counters
+ CountersTracker.setCounterRange(sharedId, numCounters - 1);
+ } catch (Throwable e) {
+ error.compareAndSet(null, e);
+ } finally {
+ doneLatch.countDown();
+ }
+ });
+ }
+
+ // Start all threads at once
+ startLatch.countDown();
+
+ // Wait for completion
+ assertTrue("Threads didn't finish in time", doneLatch.await(30, TimeUnit.SECONDS));
+ executor.shutdown();
+
+ if (error.get() != null) {
+ fail("Concurrent allocation failed: " + error.get().getMessage());
+ }
+ }
+
+ @Test
+ public void testMaximizePattern() {
+ // Test the typical usage pattern for maximize()
+ int id = 400000;
+ int numCounters = 101; // For range [0, 100]
+
+ CountersTracker.ensureCountersAllocated(id, numCounters);
+
+ // Simulate maximize(50, id, 0, 100) - should set counters 0-50 to 1
+ int value = 50;
+ int minValue = 0;
+ int steps = value - minValue;
+ CountersTracker.setCounterRange(id, steps);
+
+ // Should be able to call again with higher value
+ value = 75;
+ steps = value - minValue;
+ CountersTracker.setCounterRange(id, steps);
+ }
+}