diff --git a/README.md b/README.md index f055977..8c31c70 100644 --- a/README.md +++ b/README.md @@ -317,6 +317,10 @@ The `close()` and `closeAsync()` methods will ensure all events have been publis context.close(); ``` +### Switchback Experiments + +Switchback experiments need no code changes: `getTreatment`, `peekTreatment` and the variable methods return the variant for the current time period, and the SDK emits one exposure per period while time moves forward. See [SWITCHBACK.md](SWITCHBACK.md) for the assignment rules, the exposure deduplication bound, the minimum SDK version and how older SDKs behave. + ### Custom Event Logger The A/B Smartly SDK can be instantiated with an event logger used for all contexts. diff --git a/SWITCHBACK.md b/SWITCHBACK.md new file mode 100644 index 0000000..c6a2ead --- /dev/null +++ b/SWITCHBACK.md @@ -0,0 +1,128 @@ +# Switchback experiments + +A switchback experiment hashes each unit, or one of its context attributes, into one of 10, 50 or +100 *buckets*. The bucket is the cluster: it is randomized into a variant that changes over time on +a fixed UTC grid, and every unit in the bucket sees the same variant within a period. + +## Minimum SDK version + +Switchback serving is supported from the first `core-api` release after **1.6.3**. Every release up +to and including 1.6.3 lacks it. This change does not bump `VERSION`; the release that ships it sets +the number. + +## What the SDK does + +The SDK reads these optional fields from each experiment in the context data. Emitting them is the +Collector's job and is a separate integration dependency: a Collector that does not emit them makes +every experiment a unit experiment for this SDK. + +| Field | Meaning | +| --- | --- | +| `randomizationType` | `"unit"` or `"switchback"`. Absent means `"unit"`. | +| `switchbackClusterUnitType` | Cluster unit type name. Must equal `unitType` in Phase 1. | +| `switchbackStartAt` | Grid start, epoch milliseconds. Null before the experiment starts. | +| `switchbackPeriodMs` | Period length in milliseconds (whole hours). | +| `switchbackCarryoverDurationMs`, `switchbackBurnInDurationMs`, `switchbackBurnOutDurationMs` | Analysis-only windows. The SDK carries them but does not use them. | +| `switchbackDesignType`, `switchbackEstimand` | Design metadata. | +| `switchbackClusterCount` | Number of buckets: 10, 50 or 100. | +| `switchbackClusterAttribute` | Attribute path hashed into buckets. Null hashes the unit. | + +For a switchback experiment: + +- `anchor = switchbackStartAt` rounded up to the UTC hour, and + `periodIndex = floorDiv(now - anchor, switchbackPeriodMs)`, where `now` is one reading of the + context's `Clock` taken by the `getTreatment`, `peekTreatment`, `getVariableValue` or + `peekVariableValue` call. +- The cluster key is `hashUnit(uid)`, the MD5/base64url string the SDK publishes as the hashed + unit, when `switchbackClusterAttribute` is null. Otherwise it is the context attribute at that + path (see below): a non-empty string as-is, or an integer in base 10. +- `bucket = u32be(MD5(utf8("sbcluster" + U+001F + seedHi + ":" + seedLo + U+001F + key))[0..3]) mod + switchbackClusterCount`, pinned by `core-api/src/test/resources/switchback/bucket-golden-vectors.json`. +- The variant is the balanced-block draw + `assignSwitchback(String.valueOf(bucket), periodIndex, seed, split.length)`, with + `seed = (seedHi << 32) | (seedLo & 0xffffffff)`, pinned by + `core-api/src/test/resources/switchback/golden-vectors.json`. Both functions are the Collector's. +- Precedence is the same as for unit experiments: override, holdouts, audience, full-on, traffic + eligibility, then custom assignment or the switchback draw. Custom assignments therefore apply only + to traffic-eligible units. Traffic eligibility uses the per-unit traffic hash, so it is sticky per + unit across periods; units in the same bucket can differ in traffic eligibility. +- The experiment is ineligible (variant 0) before the anchor, when `switchbackStartAt` or + `switchbackPeriodMs` is null, when the cluster unit type differs from the unit type, when + `switchbackClusterCount` is not 10, 50 or 100, when `switchbackClusterAttribute` is not a + supported path, or when the unit has no cluster key: the attribute is absent, empty, a boolean, + a floating-point number, an object or an array. Custom assignments need a cluster key too. If the + experiment unit is not set on the context, the experiment is unassigned. The SDK never falls back + to unit assignment. +- Attribute paths follow the Collector: `/` separates segments, a segment quoted with `'` or `"` is + a literal key, and an unquoted integer segment (`items/0/city`) indexes a list. An index must be + non-negative and sit between two key segments. The first segment names an attribute set with + `setAttribute`; deeper segments read its value as the Collector reads its JSON encoding. Among + attributes with that name set at or before the call's clock reading, the one with the latest + `setAt` wins, and on a tie the one set last. +- The cluster key is read once per period slot. A `setAttribute` later in the same period does not + move the unit to another bucket, even when an audience change re-resolves the slot; the next + period reads the attribute again. +- The exposure and publish payloads are unchanged. Analysis derives the period from `exposedAt`, so + a switchback exposure is stamped with the clock reading that selected its period, not a later + one. A call that resolves just before a boundary and exposes just after it still publishes a + timestamp inside the period whose variant it returned. Each exposing call uses its own reading, so + an exposure that follows an earlier `peekTreatment` carries the exposing call's time. Unit + experiments and holdout exposures are stamped when the exposure is enqueued, as before. + Exposures continue during burn windows. + +### Exposure deduplication + +Each period gets its own cached assignment and exposure state. The context retains **at most two +period indices** per experiment. On installing a third, it evicts the lowest index other than the +one just installed. Retention is by index, not recency: a cache hit does not protect a period from +eviction. + +- In normal forward time, the SDK emits one exposure per experiment, cluster and period. After a + boundary, the next `getTreatment`/`getVariableValue` returns the new period's variant and emits a + new exposure. +- Dedup holds only within the retained window. Re-entering an evicted period emits one more + exposure for that period. This happens when the clock moves back by two or more periods, or a + caller reads the clock and is then delayed until two later periods have been resolved. The + new assignment is cached again, so re-entry costs one extra exposure per period re-entered, + never one per call. Provided seed, split and custom assignment are unchanged, the duplicate has + the same variant and lies in the same period, so analysis sees it as a repeat. If any of them + changed in between, the recreated assignment uses the current values and can carry a different + variant. Deduplication on re-entry is a bounded-cache best effort, not a guarantee against + mixed-variant records for a period. +- Pre-anchor and unevaluable-grid resolutions share one slot per experiment, so they expose once. + +### Refresh and epochs + +An experiment's switchback identity is `randomizationType`, `switchbackClusterUnitType`, +`switchbackStartAt`, `switchbackPeriodMs`, `switchbackDesignType`, `switchbackClusterCount`, +`switchbackClusterAttribute`, its id and its iteration. The +backend rejects changes to the grid fields after an experiment starts, so within a live experiment +a change means a pre-start edit, a restart (new iteration), or a type change. + +On each refresh, an installed switchback keeps its previous generation only when its previous +definition was a switchback with the same identity; otherwise it receives a fresh generation. Unit +experiments never receive one, so their caches are unaffected. Because removal or a unit definition +breaks the chain, a switchback that returns later with an identical definition gets a new +generation, whether or not the context was queried in between. Cached switchback assignments record +the generation they were resolved under, and the next resolution after a generation change drops +every one of that experiment's switchback slots together, including the pre-anchor slot and +override assignments. The change starts a new epoch, and the next call re-resolves and exposes again +even if the period number is unchanged. + +Invalidation is lazy: slots are dropped on the experiment's next resolution, not during refresh, +which takes only the data lock. An experiment that is removed and never queried again keeps its +retained slots (at most two periods plus one pre-anchor slot) for the life of the context, just as +an exposed unit-experiment assignment does. +Changes to burn, carryover and estimand values do not +start a new epoch. Seed and split edits do not invalidate an already cached assignment; the +next new period uses them. + +## Older SDKs + +No capability filter exists, so older SDKs receive switchback experiments too. They ignore the +unknown fields and serve a sticky unit assignment from `split`/`seedHi`/`seedLo`, which does not +change over time. Their exposures are well-formed but generally do not match the expected +switchback schedule. Analysis is designed to reconcile each exposure block against the expected +schedule and report mismatching blocks. The platform's reconciliation query returns the observed +per-block variants, and the expected-schedule comparison is an engine-side step. Upgrade every SDK +that serves a switchback experiment rather than relying on that reconciliation. diff --git a/core-api/src/main/java/com/absmartly/sdk/Context.java b/core-api/src/main/java/com/absmartly/sdk/Context.java index 046af46..ecaf798 100644 --- a/core-api/src/main/java/com/absmartly/sdk/Context.java +++ b/core-api/src/main/java/com/absmartly/sdk/Context.java @@ -26,6 +26,9 @@ import com.absmartly.sdk.internal.Algorithm; import com.absmartly.sdk.internal.Concurrency; +import com.absmartly.sdk.internal.SwitchbackAssigner; +import com.absmartly.sdk.internal.SwitchbackAttributePath; +import com.absmartly.sdk.internal.SwitchbackBuckets; import com.absmartly.sdk.internal.VariantAssigner; import com.absmartly.sdk.internal.hashing.Hashing; import com.absmartly.sdk.java.nio.charset.StandardCharsets; @@ -38,6 +41,11 @@ public class Context implements Closeable { private static final int ASSIGNMENT_EXPOSED = 1; private static final int ASSIGNMENT_RETIRED = 2; private static final int MAX_EXPOSURE_ATTEMPTS = 3; + private static final String RANDOMIZATION_SWITCHBACK = "switchback"; + private static final char SWITCHBACK_KEY_SEPARATOR = '\u001f'; + private static final long NO_SWITCHBACK_PERIOD = Long.MIN_VALUE; + private static final long HOUR_MS = 3600000L; + private static final int SWITCHBACK_RETAINED_PERIODS = 2; private static final Logger log = LoggerFactory.getLogger(Context.class); public static Context create(@Nonnull final Clock clock, @Nonnull final ContextConfig config, @@ -491,18 +499,16 @@ public void setAttributes(@Nonnull final Map attributes) { public int getTreatment(@Nonnull final String experimentName) { checkReady(true); - return getExposedTreatmentVariant(getAssignment(experimentName)); + final long now = clock_.millis(); + return exposeTreatmentAssignment(experimentName, getAssignment(experimentName, now), now).variant; } - private int getExposedTreatmentVariant(final Assignment assignment) { - return exposeTreatmentAssignment(assignment).variant; - } - - private Assignment exposeTreatmentAssignment(final Assignment assignment) { - return settleExposure(assignment, new Function() { + private Assignment exposeTreatmentAssignment(final String experimentName, final Assignment assignment, + final long resolvedAt) { + return settleExposure(assignment, resolvedAt, new Function() { @Override - public Assignment apply(final Assignment retired) { - return getAssignment(retired.name); + public Assignment apply(final Long now) { + return getAssignment(experimentName, now); } }); } @@ -516,12 +522,16 @@ public Assignment apply(final Assignment retired) { // time we get here, so a skipped holdout trigger would never be retried for this context's // life. Failures are collected and re-thrown once every holdout has had a chance to fire, // rather than swallowed or allowed to abort the loop early. - private int triggerExposure(final Assignment assignment) { + // + // A switchback exposure is stamped with resolvedAt, the clock reading that selected its period: + // analysis derives the period from exposedAt, so a later reading could attribute this variant + // to the next period. Unit experiments read the clock at enqueue time. + private int triggerExposure(final Assignment assignment, final long resolvedAt) { if (assignment.exposureState.compareAndSet(ASSIGNMENT_UNEXPOSED, ASSIGNMENT_EXPOSED)) { RuntimeException failure = null; try { if (!assignment.suppressed) { - enqueueExposure(assignment); + enqueueExposure(assignment, assignment.switchback ? resolvedAt : clock_.millis()); } } catch (final RuntimeException e) { failure = e; @@ -579,7 +589,7 @@ private void triggerHoldoutExposure(final Experiment holdoutExperiment, final St private void triggerHoldoutExposure(final Assignment holdoutAssignment) { if ((holdoutAssignment != null) && holdoutAssignment.exposureState.compareAndSet(ASSIGNMENT_UNEXPOSED, ASSIGNMENT_EXPOSED)) { - enqueueExposure(holdoutAssignment); + enqueueExposure(holdoutAssignment, clock_.millis()); } } @@ -589,13 +599,13 @@ private void triggerHoldoutExposure(final Assignment holdoutAssignment) { // without ever enqueueing its own (e.g. an already-exposed winner, or a losing variable-key // candidate that only fires holdouts), and setTimeout() is idempotent, so centralizing the // call is strictly safer than tracking every call site individually. - private void enqueueExposure(final Assignment assignment) { + private void enqueueExposure(final Assignment assignment, final long exposedAt) { final Exposure exposure = new Exposure(); exposure.id = assignment.id; exposure.name = assignment.name; exposure.unit = assignment.unitType; exposure.variant = assignment.variant; - exposure.exposedAt = clock_.millis(); + exposure.exposedAt = exposedAt; exposure.assigned = assignment.assigned; exposure.eligible = assignment.eligible; exposure.overridden = assignment.overridden; @@ -624,7 +634,7 @@ private void enqueueExposure(final Assignment assignment) { public int peekTreatment(@Nonnull final String experimentName) { checkReady(true); - return getAssignment(experimentName).variant; + return getAssignment(experimentName, clock_.millis()).variant; } public Map> getVariableKeys() { @@ -653,7 +663,8 @@ public Map> getVariableKeys() { public Object getVariableValue(@Nonnull final String key, final Object defaultValue) { checkReady(true); - final Assignment assignment = exposeVariableAssignment(key, getVariableAssignment(key, false)); + final long now = clock_.millis(); + final Assignment assignment = exposeVariableAssignment(key, getVariableAssignment(key, false, now), now); if (assignment != null) { if (assignment.variables != null) { if (assignment.variables.containsKey(key)) { @@ -664,26 +675,30 @@ public Object getVariableValue(@Nonnull final String key, final Object defaultVa return defaultValue; } - private Assignment exposeVariableAssignment(final String key, final Assignment assignment) { - return settleExposure(assignment, new Function() { + private Assignment exposeVariableAssignment(final String key, final Assignment assignment, final long resolvedAt) { + return settleExposure(assignment, resolvedAt, new Function() { @Override - public Assignment apply(final Assignment retired) { - return getVariableAssignment(key, false); + public Assignment apply(final Long now) { + return getVariableAssignment(key, false, now); } }); } - private Assignment settleExposure(final Assignment assignment, final Function resolver) { + // Each retry after a concurrent retirement resolves at a fresh clock reading and exposes at it. + private Assignment settleExposure(final Assignment assignment, final long resolvedAt, + final Function resolver) { Assignment current = assignment; + long now = resolvedAt; for (int attempt = 0; current != null; ++attempt) { - if (triggerExposure(current) != ASSIGNMENT_RETIRED) { + if (triggerExposure(current, now) != ASSIGNMENT_RETIRED) { return current; } if (attempt + 1 >= MAX_EXPOSURE_ATTEMPTS) { // Exhaustion returns the attempted assignment that lost to a concurrent retirement. return current; } - current = resolver.apply(current); + now = clock_.millis(); + current = resolver.apply(now); } return current; } @@ -691,7 +706,7 @@ private Assignment settleExposure(final Assignment assignment, final Function= selected.setAt))) { + selected = attribute; + } + } + return selected; + } + + // Unit experiments, and switchback experiments with no current period, keep the bare name. + private static String assignmentCacheKey(final String experimentName, final long periodIndex) { + if (periodIndex == NO_SWITCHBACK_PERIOD) { + return experimentName; + } + return switchbackCacheKey(experimentName, periodIndex); + } + + private static String switchbackCacheKey(final String experimentName, final long periodIndex) { + return experimentName + SWITCHBACK_KEY_SEPARATOR + periodIndex; + } + + // Switchback identity: id, iteration, unit type, cluster unit type, startAt, periodMs, design, + // cluster count and cluster attribute, i.e. every input to the period grid and the cluster key. + // Seed, split and analysis-only fields are excluded. Absent and unit experiments have no + // identity. + private static boolean sameSwitchbackIdentity(final ContextExperiment a, final ContextExperiment b) { + final boolean aSwitchback = (a != null) && isSwitchback(a.data); + final boolean bSwitchback = (b != null) && isSwitchback(b.data); + if (!aSwitchback || !bSwitchback) { + return aSwitchback == bSwitchback; + } + return (a.data.id == b.data.id) && (a.data.iteration == b.data.iteration) + && Objects.equals(a.data.unitType, b.data.unitType) + && Objects.equals(a.data.switchbackClusterUnitType, b.data.switchbackClusterUnitType) + && Objects.equals(a.data.switchbackStartAt, b.data.switchbackStartAt) + && Objects.equals(a.data.switchbackPeriodMs, b.data.switchbackPeriodMs) + && Objects.equals(a.data.switchbackDesignType, b.data.switchbackDesignType) + && Objects.equals(a.data.switchbackClusterCount, b.data.switchbackClusterCount) + && Objects.equals(a.data.switchbackClusterAttribute, b.data.switchbackClusterAttribute); + } + + // Lifecycle of one switchback experiment's cache slots: the bare-name slot (pre-anchor or an + // unevaluable grid) and its period slots, all created under one switchback generation (see + // ContextExperiment.switchbackGeneration). Indices from different generations are never + // compared: a changed start or period length re-maps them. + private static final class SwitchbackEpoch { + final long generation; + final TreeSet periods = new TreeSet(); + + SwitchbackEpoch(final long generation) { + this.generation = generation; + } + } + + // An epoch is stale once the installed definition is absent, not switchback, or carries a + // different generation. Every identity transition, including an intermediate unit or removed + // state that was never looked up, assigns a fresh generation, so a stale epoch never matches a + // later identical definition. Caller holds contextLock_ (read or write). + private boolean isSwitchbackEpochStale(final String experimentName, final ContextExperiment experiment) { + if (switchbackEpochs_.isEmpty()) { + return false; + } + final SwitchbackEpoch epoch = switchbackEpochs_.get(experimentName); + return (epoch != null) && ((experiment == null) || !isSwitchback(experiment.data) + || (epoch.generation != experiment.switchbackGeneration)); + } + + // Drops every slot of the epoch, bare-name slot included. Caller holds the contextLock_ write lock. + private void dropSwitchbackEpoch(final String experimentName) { + final SwitchbackEpoch epoch = switchbackEpochs_.remove(experimentName); + if (epoch != null) { + for (final Long period : epoch.periods) { + assignmentCache_.remove(switchbackCacheKey(experimentName, period)); + } + assignmentCache_.remove(experimentName); + } + } + + // Records a switchback resolution under the current epoch. At most SWITCHBACK_RETAINED_PERIODS + // period indices are kept; the lowest index other than periodIndex is evicted, so the entry + // just installed always survives. Exposure dedup therefore holds only within the retained + // window: re-entering an evicted period (clock rewind, or a caller delayed across two + // boundaries) creates one new assignment for it, which then stays cached like any other. + private void retainSwitchbackPeriod(final String experimentName, final ContextExperiment experiment, + final long periodIndex) { + SwitchbackEpoch epoch = switchbackEpochs_.get(experimentName); + if (epoch == null) { + epoch = new SwitchbackEpoch(experiment.switchbackGeneration); + switchbackEpochs_.put(experimentName, epoch); + } + if (periodIndex == NO_SWITCHBACK_PERIOD) { + return; + } + epoch.periods.add(periodIndex); + while (epoch.periods.size() > SWITCHBACK_RETAINED_PERIODS) { + Long evicted = epoch.periods.first(); + if (evicted == periodIndex) { + evicted = epoch.periods.higher(periodIndex); + } + epoch.periods.remove(evicted); + assignmentCache_.remove(switchbackCacheKey(experimentName, evicted)); + } } // Applicable-holdout identity for cache-validity purposes is (id, iteration) per entry, in @@ -1109,6 +1328,19 @@ private static class Assignment { Map variables = Collections.emptyMap(); int attrsSeq; + // Switchback grid this assignment was resolved under; compared by switchbackConfigMatches. + boolean switchback; + String switchbackClusterUnitType; + Long switchbackStartAt; + Long switchbackPeriodMs; + String switchbackDesignType; + Integer switchbackClusterCount; + String switchbackClusterAttribute; + // Set once the cluster key was read for this slot; switchbackClusterKey is then the key, or + // null for a unit without one. + boolean switchbackClusterKeyResolved; + String switchbackClusterKey; + final AtomicInteger exposureState = new AtomicInteger(ASSIGNMENT_UNEXPOSED); } @@ -1172,42 +1404,44 @@ boolean matches(Experiment current) { } } - private Assignment getAssignment(final String experimentName) { + private boolean isAssignmentCurrent(final Assignment assignment, final Integer custom, final Integer override, + final ContextExperiment experiment) { + if (override != null) { + // An override still evaluates the experiment for holdout purposes (see the + // write path in getAssignment), so the fast path must revalidate the applicable-holdout + // set exactly like the ordinary branch does via experimentMatches - otherwise + // a holdout that becomes applicable after a refresh never fires for an + // already-overridden experiment, and stays that way forever. The switchback identity is + // revalidated too, so a grid change re-resolves an overridden slot like any other. + return assignment.overridden && assignment.variant == override + && holdoutSetMatches((experiment != null) ? experiment.holdouts : null, assignment.holdouts) + && ((experiment != null) ? switchbackConfigMatches(experiment.data, assignment) + : !assignment.switchback); + } else if (experiment == null) { + // previously not-running experiment + return !assignment.assigned; + } else if (Objects.equals(custom, assignment.customAssignment)) { + return experimentMatches(experiment, assignment) && audienceMatches(experiment.data, assignment); + } + return false; + } + + // now is the caller's single clock reading for this resolution, so the cache key, the drawn + // period and a switchback exposure's exposedAt all agree. + private Assignment getAssignment(final String experimentName, final long now) { final ReentrantReadWriteLock.ReadLock readLock = contextLock_.readLock(); try { readLock.lock(); - final Assignment assignment = assignmentCache_.get(experimentName); - - if (assignment != null) { - final Integer custom = cassignments_.get(experimentName); - final Integer override = overrides_.get(experimentName); - final ContextExperiment experiment = Context.this.getExperiment(experimentName); - - if (override != null) { - // An override still evaluates the experiment for holdout purposes (see the - // write path below), so the fast path must revalidate the applicable-holdout - // set exactly like the ordinary branch does via experimentMatches - otherwise - // a holdout that becomes applicable after a refresh never fires for an - // already-overridden experiment, and stays that way forever. - if (assignment.overridden && assignment.variant == override - && holdoutSetMatches((experiment != null) ? experiment.holdouts : null, - assignment.holdouts)) { - // override and holdout coverage both up-to-date - return assignment; - } - } else if (experiment == null) { - if (!assignment.assigned) { - // previously not-running experiment - return assignment; - } - } else if (Objects.equals(custom, assignment.customAssignment)) { - if (experimentMatches(experiment, assignment) - && audienceMatches(experiment.data, assignment)) { - // assignment up-to-date - return assignment; - } - } + final ContextExperiment experiment = Context.this.getExperiment(experimentName); + final Assignment assignment = assignmentCache_ + .get(assignmentCacheKey(experimentName, currentSwitchbackPeriod(experiment, now))); + + // A stale epoch's slots are dropped by the write path below. + if ((assignment != null) && !isSwitchbackEpochStale(experimentName, experiment) + && isAssignmentCurrent(assignment, cassignments_.get(experimentName), + overrides_.get(experimentName), experiment)) { + return assignment; } } finally { readLock.unlock(); @@ -1221,11 +1455,30 @@ && audienceMatches(experiment.data, assignment)) { final Integer custom = cassignments_.get(experimentName); final Integer override = overrides_.get(experimentName); final ContextExperiment experiment = Context.this.getExperiment(experimentName); + final long periodIndex = currentSwitchbackPeriod(experiment, now); + final String cacheKey = assignmentCacheKey(experimentName, periodIndex); + + if (isSwitchbackEpochStale(experimentName, experiment)) { + dropSwitchbackEpoch(experimentName); + } + + // Recheck: a concurrent caller may have installed this key (e.g. the first call in a + // new switchback period) between the two locks; replacing it would reset its exposure. + final Assignment cached = assignmentCache_.get(cacheKey); + if ((cached != null) && isAssignmentCurrent(cached, custom, override, experiment)) { + return cached; + } final Assignment assignment = new Assignment(); assignment.name = experimentName; assignment.eligible = true; assignment.customAssignment = custom; + // Within one period slot the cluster key is read once: a slot replaced in + // the same epoch, e.g. after setAttribute changed the audience, keeps its bucket. + if ((cached != null) && cached.switchbackClusterKeyResolved) { + assignment.switchbackClusterKeyResolved = true; + assignment.switchbackClusterKey = cached.switchbackClusterKey; + } if (override != null) { if (experiment != null) { @@ -1237,6 +1490,7 @@ && audienceMatches(experiment.data, assignment)) { // assignment. Override precedence itself is untouched: the returned variant // stays the overridden one. assignment.holdouts = experiment.holdouts; + pinSwitchbackConfig(assignment, experiment.data); } assignment.overridden = true; @@ -1244,6 +1498,7 @@ && audienceMatches(experiment.data, assignment)) { } else { if (experiment != null) { final String unitType = experiment.data.unitType; + pinSwitchbackConfig(assignment, experiment.data); // Share the experiment's applicable-holdouts array by reference rather than // copying: it is only read here (and via Arrays.equals in experimentMatches) and @@ -1302,25 +1557,51 @@ && audienceMatches(experiment.data, assignment)) { if (uid != null) { final byte[] unitHash = Context.this.getUnitHash(unitType, uid); - final VariantAssigner assigner = Context.this.getVariantAssigner(unitType, - unitHash); - final boolean eligible = assigner.assign(experiment.data.trafficSplit, - experiment.data.trafficSeedHi, - experiment.data.trafficSeedLo) == 1; - if (eligible) { - if (custom != null) { - assignment.variant = custom; - assignment.custom = true; - } else { - assignment.variant = assigner.assign(experiment.data.split, - experiment.data.seedHi, - experiment.data.seedLo); - } - } else { + if (assignment.switchback && (periodIndex == NO_SWITCHBACK_PERIOD)) { + // Before the grid anchor, or a grid this SDK cannot evaluate: + // ineligible like a traffic miss, never a unit-assignment draw. assignment.eligible = false; assignment.variant = 0; + assignment.assigned = true; + } else { + final VariantAssigner assigner = Context.this.getVariantAssigner(unitType, + unitHash); + boolean eligible = assigner.assign(experiment.data.trafficSplit, + experiment.data.trafficSeedHi, + experiment.data.trafficSeedLo) == 1; + String clusterKey = null; + if (eligible && assignment.switchback) { + if (!assignment.switchbackClusterKeyResolved) { + assignment.switchbackClusterKeyResolved = true; + assignment.switchbackClusterKey = switchbackClusterKey(experiment, + unitHash, now); + } + clusterKey = assignment.switchbackClusterKey; + // No key: ineligible, as the Collector serves it. + eligible = clusterKey != null; + } + if (eligible) { + if (custom != null) { + assignment.variant = custom; + assignment.custom = true; + } else if (assignment.switchback) { + final int bucket = SwitchbackBuckets.bucketOf(experiment.data.seedHi, + experiment.data.seedLo, clusterKey, + experiment.data.switchbackClusterCount); + assignment.variant = SwitchbackAssigner.assign(Integer.toString(bucket), + periodIndex, switchbackSeed(experiment.data), + experiment.data.split.length); + } else { + assignment.variant = assigner.assign(experiment.data.split, + experiment.data.seedHi, + experiment.data.seedLo); + } + } else { + assignment.eligible = false; + assignment.variant = 0; + } + assignment.assigned = true; } - assignment.assigned = true; } } else { assignment.assigned = true; @@ -1343,7 +1624,10 @@ && audienceMatches(experiment.data, assignment)) { assignment.variables = experiment.variables.get(assignment.variant); } - assignmentCache_.put(experimentName, assignment); + assignmentCache_.put(cacheKey, assignment); + if ((experiment != null) && isSwitchback(experiment.data)) { + retainSwitchbackPeriod(experimentName, experiment, periodIndex); + } return assignment; } finally { @@ -1385,14 +1669,14 @@ private static boolean isHeldOutBy(final int holdoutVariant, final int holdoutAr // ends up the one returned, so on the non-peek path each candidate's applicable holdouts fire // as it is visited - the ordinary experiment exposure is still emitted only for the winner, // by the caller. peek must stay side-effect free, so it skips triggering entirely. - private Assignment getVariableAssignment(final String key, final boolean peek) { + private Assignment getVariableAssignment(final String key, final boolean peek, final long now) { final List keyExperimentVariables = getVariableExperiments(key); if (keyExperimentVariables != null) { Assignment suppressedFallback = null; RuntimeException failure = null; for (final ContextExperiment experimentVariables : keyExperimentVariables) { - final Assignment assignment = getAssignment(experimentVariables.data.name); + final Assignment assignment = getAssignment(experimentVariables.data.name, now); if (!peek) { final RuntimeException holdoutFailure = triggerApplicableHoldoutExposures(assignment); if (failure == null) { @@ -1607,6 +1891,11 @@ private static class ContextExperiment { Experiment[] holdouts; List> variables; Map customFieldValues; + // Assigned in setData under dataLock_ write: carried over from the previous index while the + // switchback identity is unchanged, otherwise a fresh value. Unused for unit experiments. + long switchbackGeneration; + // Parsed switchbackClusterAttribute; null for the unit source or an unsupported path. + SwitchbackAttributePath switchbackClusterPath; } private static class ContextCustomFieldValue { @@ -1665,6 +1954,10 @@ private void setData(final ContextData data) { final ContextExperiment contextExperiment = new ContextExperiment(); contextExperiment.data = experiment; contextExperiment.holdouts = resolveApplicableHoldouts(experiment, holdoutsById); + if (isSwitchback(experiment)) { + contextExperiment.switchbackClusterPath = SwitchbackAttributePath + .parseSupported(experiment.switchbackClusterAttribute); + } contextExperiment.variables = new ArrayList>( experiment.variants != null ? experiment.variants.length : 0); @@ -1749,6 +2042,16 @@ public int compare(ContextExperiment a, ContextExperiment b) { try { dataLock_.writeLock().lock(); + for (final Map.Entry entry : index.entrySet()) { + final ContextExperiment installed = entry.getValue(); + if (isSwitchback(installed.data)) { + final ContextExperiment previous = (index_ != null) ? index_.get(entry.getKey()) : null; + installed.switchbackGeneration = sameSwitchbackIdentity(previous, installed) + ? previous.switchbackGeneration + : ++switchbackGenerationCounter_; + } + } + index_ = index; indexVariables_ = indexVariables; holdoutsById_ = holdoutsById; @@ -1821,11 +2124,16 @@ private Map buildAttributesMap() { // Id-keyed view of the holdouts installed by the most recent setData, used to resolve a // holdout's live definition independent of any stale Experiment reference a caller holds. private Map holdoutsById_; + // Source of ContextExperiment.switchbackGeneration; guarded by dataLock_ write. + private long switchbackGenerationCounter_; private final ReentrantReadWriteLock contextLock_ = new ReentrantReadWriteLock(); private final Map hashedUnits_; private final Map assigners_; + // Keyed by experiment name, or by name + U+001F + period index for a switchback experiment + // inside its grid (see assignmentCacheKey), so each period gets its own exposure state. private final Map assignmentCache_ = new HashMap(); + private final Map switchbackEpochs_ = new HashMap(); // A holdout's identity, for both this cache's keying and for detecting whether a covered // experiment's own set of applicable holdouts is still current (holdoutSetMatches), is its // (id, iteration) pair - never a full Experiment comparison. Rationale: the governing diff --git a/core-api/src/main/java/com/absmartly/sdk/internal/SwitchbackAssigner.java b/core-api/src/main/java/com/absmartly/sdk/internal/SwitchbackAssigner.java new file mode 100644 index 0000000..eb7be47 --- /dev/null +++ b/core-api/src/main/java/com/absmartly/sdk/internal/SwitchbackAssigner.java @@ -0,0 +1,78 @@ +package com.absmartly.sdk.internal; + +import com.absmartly.sdk.internal.hashing.Murmur3_32; + +// Balanced-block switchback assignment. Port of the platform's +// com.absmartly.lib.analytics.switchback.SwitchbackAssignment (Guava murmur3_32_fixed, seed 0): +// key = utf8(clusterUnit + "\u001f" + cycleIndex) +// baseHash = murmur3_32(le64(seed) ++ le32(murmur3_32(key))) +// stepHash = murmur3_32(le32(baseHash) ++ le32(i)), i = armCount-1 .. 1 +// j = min(i, floor(u32(stepHash) / 0xffffffff * (i + 1))) +// variant = permutation[floorMod(periodIndex, armCount)], cycle = floorDiv(periodIndex, armCount) +// Any change here breaks parity with the platform golden vectors and the analysis-side schedule. +public abstract class SwitchbackAssigner { + private SwitchbackAssigner() {} + + private static final double NORMALIZER = 1.0 / 0xffffffffL; + private static final char SEPARATOR = '\u001f'; + + public static int assign(final CharSequence clusterUnit, final long periodIndex, final long seed, + final int armCount) { + requireValidArmCount(armCount); + final long cycleIndex = floorDiv(periodIndex, armCount); + // floorMod; the product may wrap near Long.MIN_VALUE but the difference is exact mod 2^64. + final int cycleSlot = (int) (periodIndex - cycleIndex * armCount); + return fisherYatesPermutation(armCount, baseHash(clusterUnit, cycleIndex, seed))[cycleSlot]; + } + + public static int baseHash(final CharSequence clusterUnit, final long cycleIndex, final long seed) { + final String key = new StringBuilder(clusterUnit.length() + 21).append(clusterUnit).append(SEPARATOR) + .append(cycleIndex).toString(); + final byte[] keyBytes = new byte[key.length() * 4]; + final int keyLen = Buffers.encodeUTF8(keyBytes, 0, key); + final int keyHash = Murmur3_32.digest(keyBytes, 0, keyLen, 0); + + final byte[] buffer = new byte[12]; + Buffers.putUInt32(buffer, 0, (int) seed); + Buffers.putUInt32(buffer, 4, (int) (seed >>> 32)); + Buffers.putUInt32(buffer, 8, keyHash); + return Murmur3_32.digest(buffer, 0); + } + + public static int[] fisherYatesPermutation(final int armCount, final int baseHash) { + requireValidArmCount(armCount); + + final int[] permutation = new int[armCount]; + for (int i = 0; i < armCount; ++i) { + permutation[i] = i; + } + + final byte[] buffer = new byte[8]; + Buffers.putUInt32(buffer, 0, baseHash); + for (int i = armCount - 1; i > 0; --i) { + Buffers.putUInt32(buffer, 4, i); + final int stepHash = Murmur3_32.digest(buffer, 0); + final double prob = (stepHash & 0xffffffffL) * NORMALIZER; + // prob == 1.0 would map to i + 1 + final int j = Math.min(i, (int) Math.floor(prob * (i + 1))); + + final int tmp = permutation[i]; + permutation[i] = permutation[j]; + permutation[j] = tmp; + } + + return permutation; + } + + // Math.floorDiv is Java 8+; divisor is always positive here. + public static long floorDiv(final long dividend, final long divisor) { + final long q = dividend / divisor; + return ((dividend % divisor) != 0 && ((dividend ^ divisor) < 0)) ? q - 1 : q; + } + + private static void requireValidArmCount(final int armCount) { + if (armCount < 2) { + throw new IllegalArgumentException("armCount must be >= 2, got " + armCount); + } + } +} diff --git a/core-api/src/main/java/com/absmartly/sdk/internal/SwitchbackAttributePath.java b/core-api/src/main/java/com/absmartly/sdk/internal/SwitchbackAttributePath.java new file mode 100644 index 0000000..7ca0c95 --- /dev/null +++ b/core-api/src/main/java/com/absmartly/sdk/internal/SwitchbackAttributePath.java @@ -0,0 +1,201 @@ +package com.absmartly.sdk.internal; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.ObjectMapper; + +// A switchback cluster attribute path, resolved exactly like the Collector's +// SwitchbackAttributePath: "/" separates segments, a segment wrapped in ' or " is a literal key, +// and an unquoted segment that Guava Ints.tryParse accepts (ASCII digits, optional leading '-') +// indexes a List and nothing else. Every other segment reads a Map key. +public final class SwitchbackAttributePath { + private static final Logger log = LoggerFactory.getLogger(SwitchbackAttributePath.class); + private static final ObjectMapper JSON = new ObjectMapper(); + private static final int MAX_DEPTH = 64; + + private final List keys; + // Parallel to keys; null where the segment is a map key. + private final List indices; + + private SwitchbackAttributePath(final List keys, final List indices) { + this.keys = keys; + this.indices = indices; + } + + // Returns null for an unsupported path: empty, or an array index that is negative, first, + // last, or directly after another index. Analysis cannot read those. + public static SwitchbackAttributePath parseSupported(final String path) { + if (path == null) { + return null; + } + final SwitchbackAttributePath parsed = parse(path); + final int n = parsed.keys.size(); + if (n == 0) { + return null; + } + for (int i = 0; i < n; ++i) { + final Integer index = parsed.indices.get(i); + if ((index != null) && ((index < 0) || (i == 0) || (i == n - 1) || (parsed.indices.get(i - 1) != null))) { + return null; + } + } + return parsed; + } + + // The attribute name the path starts at; a supported path always starts at a key segment. + public String rootKey() { + return keys.get(0); + } + + public Object resolve(final Map attributes) { + return resolveFrom(attributes, 0); + } + + // Resolves the path below rootKey() in an SDK attribute value, read as the Collector reads it: + // the value's JSON encoding decoded by Jackson into Map, List, String, Boolean, Integer, Long, + // BigInteger or Double. Returns null when the value has no JSON encoding, contains a Map, + // Collection or array cycle, or nests those deeper than MAX_DEPTH. + public Object resolveAttribute(final Object rootValue) { + if (rootValue == null) { + return null; + } + final Object decoded; + if ((rootValue instanceof String) || (rootValue instanceof Boolean) || (rootValue instanceof Integer) + || (rootValue instanceof Long)) { + // Each of these decodes back to an equal value of the same type. + decoded = rootValue; + } else { + try { + // Jackson recurses into Maps, Collections and arrays without a cycle check, so a + // cycle through them would end in a StackOverflowError. + if (!isAcyclicWithinDepth(rootValue, new IdentityHashMap(), 0)) { + log.warn("Switchback cluster attribute '{}' is cyclic or nested deeper than {}", rootKey(), + MAX_DEPTH); + return null; + } + decoded = JSON.readValue(JSON.writeValueAsBytes(rootValue), Object.class); + } catch (Exception e) { + log.warn("Switchback cluster attribute '{}' cannot be encoded as JSON: {}", rootKey(), + e.getClass().getName()); + return null; + } + } + return resolveFrom(decoded, 1); + } + + // `path` holds the containers on the current branch only, so a value shared by two siblings is + // not a cycle. + private static boolean isAcyclicWithinDepth(final Object value, final IdentityHashMap path, + final int depth) { + final Iterable children; + if (value instanceof Map) { + children = ((Map) value).values(); + } else if (value instanceof Collection) { + children = (Collection) value; + } else if (value instanceof Object[]) { + children = Arrays.asList((Object[]) value); + } else { + return true; + } + if ((depth >= MAX_DEPTH) || (path.put(value, Boolean.TRUE) != null)) { + return false; + } + for (final Object child : children) { + if (!isAcyclicWithinDepth(child, path, depth + 1)) { + return false; + } + } + path.remove(value); + return true; + } + + private Object resolveFrom(final Object root, final int first) { + Object current = root; + for (int i = first; i < keys.size(); ++i) { + current = step(current, keys.get(i), indices.get(i)); + if (current == null) { + return null; + } + } + return current; + } + + private static SwitchbackAttributePath parse(final String path) { + final List keys = new ArrayList(); + final List indices = new ArrayList(); + int i = 0; + while (i < path.length()) { + if (path.charAt(i) == '/') { + ++i; + continue; + } + + final char first = path.charAt(i); + if ((first == '\'') || (first == '"')) { + final int end = path.indexOf(first, i + 1); + if (end < 0) { + // The platform's JsonPathParser drops an unterminated quoted tail. + break; + } + keys.add(path.substring(i + 1, end)); + indices.add(null); + i = end + 1; + } else { + final int end = path.indexOf('/', i); + final int stop = (end < 0) ? path.length() : end; + final String key = path.substring(i, stop); + keys.add(key); + indices.add(tryParseInt(key)); + i = stop; + } + } + return new SwitchbackAttributePath(Collections.unmodifiableList(keys), Collections.unmodifiableList(indices)); + } + + private static Object step(final Object current, final String key, final Integer index) { + if (index != null) { + if (current instanceof List) { + final List list = (List) current; + return ((index >= 0) && (index < list.size())) ? list.get(index) : null; + } + return null; + } + if (current instanceof Map) { + return ((Map) current).get(key); + } + return null; + } + + // Guava Ints.tryParse(String): optional '-', then one or more ASCII digits, within int range; + // otherwise null. "+1" and non-ASCII digits are not numbers; "-0" is 0. + static Integer tryParseInt(final String s) { + final int length = s.length(); + final boolean negative = (length > 0) && (s.charAt(0) == '-'); + int i = negative ? 1 : 0; + if (i == length) { + return null; + } + long magnitude = 0; + for (; i < length; ++i) { + final char c = s.charAt(i); + if ((c < '0') || (c > '9')) { + return null; + } + magnitude = magnitude * 10 + (c - '0'); + if (magnitude > (1L << 31)) { + return null; + } + } + final long value = negative ? -magnitude : magnitude; + return (value > Integer.MAX_VALUE) ? null : Integer.valueOf((int) value); + } +} diff --git a/core-api/src/main/java/com/absmartly/sdk/internal/SwitchbackBuckets.java b/core-api/src/main/java/com/absmartly/sdk/internal/SwitchbackBuckets.java new file mode 100644 index 0000000..0c5d710 --- /dev/null +++ b/core-api/src/main/java/com/absmartly/sdk/internal/SwitchbackBuckets.java @@ -0,0 +1,77 @@ +package com.absmartly.sdk.internal; + +import java.math.BigInteger; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +import com.absmartly.sdk.java.nio.charset.StandardCharsets; + +// Hashed switchback cluster buckets. Port of the platform's +// com.absmartly.lib.utils.switchback.SwitchbackBuckets, which the Collector and the warehouse SQL +// also implement: +// h = MD5(utf8("sbcluster" + U+001F + seedHi + ":" + seedLo + U+001F + key))[0..3], big-endian, unsigned +// bucket = h mod count +// The bucket, written in base 10, is the cluster passed to SwitchbackAssigner.assign. Any change +// here breaks parity with switchback/bucket-golden-vectors.json and with analysis. +public abstract class SwitchbackBuckets { + private SwitchbackBuckets() {} + + private static final char SEPARATOR = '\u001f'; + + private static final ThreadLocal MD5 = new ThreadLocal() { + @Override + protected MessageDigest initialValue() { + try { + return MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + // Every Java platform is required to provide MD5. + throw new IllegalStateException(e); + } + } + }; + + private static final int[] ALLOWED_COUNTS = {10, 50, 100}; + + public static boolean isAllowedCount(final Integer count) { + if (count == null) { + return false; + } + for (final int allowed : ALLOWED_COUNTS) { + if (count == allowed) { + return true; + } + } + return false; + } + + public static int bucketOf(final int seedHi, final int seedLo, final String key, final int count) { + if (!isAllowedCount(count)) { + throw new IllegalArgumentException( + "Switchback cluster count must be one of " + Arrays.toString(ALLOWED_COUNTS) + ", got " + count); + } + final String input = new StringBuilder(key.length() + 34).append("sbcluster").append(SEPARATOR) + .append(seedHi).append(':').append(seedLo).append(SEPARATOR).append(key).toString(); + // String.getBytes, not Buffers.encodeUTF8: the platform replaces an unpaired surrogate with + // '?' and the hashed bytes must match it. + final byte[] digest = MD5.get().digest(input.getBytes(StandardCharsets.UTF_8)); + final long hash = (((digest[0] & 0xffL) << 24) | ((digest[1] & 0xffL) << 16) | ((digest[2] & 0xffL) << 8) + | (digest[3] & 0xffL)); + return (int) (hash % count); + } + + // Bucket key of an attribute value, or null when the unit is ineligible: a non-empty String + // as-is, an integral Byte/Short/Integer/Long/BigInteger in base 10, anything else null. + public static String normalizeAttribute(final Object value) { + if (value instanceof String) { + final String s = (String) value; + return s.isEmpty() ? null : s; + } else if ((value instanceof Long) || (value instanceof Integer) || (value instanceof Short) + || (value instanceof Byte)) { + return Long.toString(((Number) value).longValue()); + } else if (value instanceof BigInteger) { + return value.toString(); + } + return null; + } +} diff --git a/core-api/src/main/java/com/absmartly/sdk/json/Experiment.java b/core-api/src/main/java/com/absmartly/sdk/json/Experiment.java index f8173dd..29a3f48 100644 --- a/core-api/src/main/java/com/absmartly/sdk/json/Experiment.java +++ b/core-api/src/main/java/com/absmartly/sdk/json/Experiment.java @@ -5,6 +5,8 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; +import com.absmartly.sdk.java.util.Objects; + @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public class Experiment { @@ -33,6 +35,22 @@ public class Experiment { // experiment is not covered by any holdout. Absent on holdout entries themselves. public int[] holdoutIds; + // Switchback configuration. randomizationType is "unit" or "switchback"; null means unit. + // Instants and durations are epoch/duration milliseconds. Carryover and burn windows are + // analysis-only and never read by assignment. + public String randomizationType; + public String switchbackClusterUnitType; + public Long switchbackStartAt; + public Long switchbackPeriodMs; + public Long switchbackCarryoverDurationMs; + public Long switchbackBurnInDurationMs; + public Long switchbackBurnOutDurationMs; + public String switchbackDesignType; + public String switchbackEstimand; + // Hashed cluster buckets: count is 10, 50 or 100; a null attribute hashes the experiment unit. + public Integer switchbackClusterCount; + public String switchbackClusterAttribute; + public Experiment() {} @Override @@ -78,7 +96,33 @@ public boolean equals(Object o) { return false; if (holdoutType != null ? !holdoutType.equals(that.holdoutType) : that.holdoutType != null) return false; - return Arrays.equals(holdoutIds, that.holdoutIds); + if (!Arrays.equals(holdoutIds, that.holdoutIds)) + return false; + if (!Objects.equals(randomizationType, that.randomizationType)) + return false; + if (!Objects.equals(switchbackClusterUnitType, that.switchbackClusterUnitType)) + return false; + if (!Objects.equals(switchbackStartAt, that.switchbackStartAt)) + return false; + if (!Objects.equals(switchbackPeriodMs, that.switchbackPeriodMs)) + return false; + if (!Objects.equals(switchbackCarryoverDurationMs, that.switchbackCarryoverDurationMs)) + return false; + if (!Objects.equals(switchbackBurnInDurationMs, that.switchbackBurnInDurationMs)) + return false; + if (!Objects.equals(switchbackBurnOutDurationMs, that.switchbackBurnOutDurationMs)) + return false; + if (!Objects.equals(switchbackDesignType, that.switchbackDesignType)) + return false; + if (!Objects.equals(switchbackEstimand, that.switchbackEstimand)) + return false; + if (!Objects.equals(switchbackClusterCount, that.switchbackClusterCount)) + return false; + return Objects.equals(switchbackClusterAttribute, that.switchbackClusterAttribute); + } + + private static int hash(Object o) { + return (o != null) ? o.hashCode() : 0; } @Override @@ -101,6 +145,17 @@ public int hashCode() { result = 31 * result + Arrays.hashCode(customFieldValues); result = 31 * result + (holdoutType != null ? holdoutType.hashCode() : 0); result = 31 * result + Arrays.hashCode(holdoutIds); + result = 31 * result + hash(randomizationType); + result = 31 * result + hash(switchbackClusterUnitType); + result = 31 * result + hash(switchbackStartAt); + result = 31 * result + hash(switchbackPeriodMs); + result = 31 * result + hash(switchbackCarryoverDurationMs); + result = 31 * result + hash(switchbackBurnInDurationMs); + result = 31 * result + hash(switchbackBurnOutDurationMs); + result = 31 * result + hash(switchbackDesignType); + result = 31 * result + hash(switchbackEstimand); + result = 31 * result + hash(switchbackClusterCount); + result = 31 * result + hash(switchbackClusterAttribute); return result; } @@ -125,6 +180,17 @@ public String toString() { ", customFieldValues=" + Arrays.toString(customFieldValues) + ", holdoutType='" + holdoutType + '\'' + ", holdoutIds=" + Arrays.toString(holdoutIds) + + ", randomizationType='" + randomizationType + '\'' + + ", switchbackClusterUnitType='" + switchbackClusterUnitType + '\'' + + ", switchbackStartAt=" + switchbackStartAt + + ", switchbackPeriodMs=" + switchbackPeriodMs + + ", switchbackCarryoverDurationMs=" + switchbackCarryoverDurationMs + + ", switchbackBurnInDurationMs=" + switchbackBurnInDurationMs + + ", switchbackBurnOutDurationMs=" + switchbackBurnOutDurationMs + + ", switchbackDesignType='" + switchbackDesignType + '\'' + + ", switchbackEstimand='" + switchbackEstimand + '\'' + + ", switchbackClusterCount=" + switchbackClusterCount + + ", switchbackClusterAttribute='" + switchbackClusterAttribute + '\'' + '}'; } } diff --git a/core-api/src/test/java/com/absmartly/sdk/ContextHoldoutTest.java b/core-api/src/test/java/com/absmartly/sdk/ContextHoldoutTest.java index 77b05f3..72e533a 100644 --- a/core-api/src/test/java/com/absmartly/sdk/ContextHoldoutTest.java +++ b/core-api/src/test/java/com/absmartly/sdk/ContextHoldoutTest.java @@ -221,28 +221,33 @@ Exposure holdoutExposure(String unitType, int id, String name, int variant) { } Object getAssignment(Context context, String experimentName) throws Exception { - final Method method = Context.class.getDeclaredMethod("getAssignment", String.class); + final Method method = Context.class.getDeclaredMethod("getAssignment", String.class, long.class); method.setAccessible(true); - return method.invoke(context, experimentName); + return method.invoke(context, experimentName, clock.millis()); } Object exposeTreatmentAssignment(Context context, Object assignment) throws Exception { - final Method method = Context.class.getDeclaredMethod("exposeTreatmentAssignment", assignment.getClass()); + final Method method = Context.class.getDeclaredMethod("exposeTreatmentAssignment", String.class, + assignment.getClass(), long.class); method.setAccessible(true); - return method.invoke(context, assignment); + return method.invoke(context, assignmentName(assignment), assignment, clock.millis()); } int getExposedTreatmentVariant(Context context, Object assignment) throws Exception { - final Method method = Context.class.getDeclaredMethod("getExposedTreatmentVariant", assignment.getClass()); - method.setAccessible(true); - return (Integer) method.invoke(context, assignment); + return assignmentVariant(exposeTreatmentAssignment(context, assignment)); } Object exposeVariableAssignment(Context context, String key, Object assignment) throws Exception { final Method method = Context.class.getDeclaredMethod("exposeVariableAssignment", String.class, - assignment.getClass()); + assignment.getClass(), long.class); method.setAccessible(true); - return method.invoke(context, key, assignment); + return method.invoke(context, key, assignment, clock.millis()); + } + + String assignmentName(Object assignment) throws Exception { + final Field field = assignment.getClass().getDeclaredField("name"); + field.setAccessible(true); + return (String) field.get(assignment); } void retireAssignment(Object assignment) throws Exception { @@ -258,9 +263,10 @@ int assignmentVariant(Object assignment) throws Exception { } Object settleExposure(Context context, Object assignment, Function resolver) throws Exception { - final Method method = Context.class.getDeclaredMethod("settleExposure", assignment.getClass(), Function.class); + final Method method = Context.class.getDeclaredMethod("settleExposure", assignment.getClass(), long.class, + Function.class); method.setAccessible(true); - return method.invoke(context, assignment, resolver); + return method.invoke(context, assignment, clock.millis(), resolver); } // A held-out unit gets control values and emits only the holdout exposure. diff --git a/core-api/src/test/java/com/absmartly/sdk/ContextSwitchbackClusterTest.java b/core-api/src/test/java/com/absmartly/sdk/ContextSwitchbackClusterTest.java new file mode 100644 index 0000000..da459af --- /dev/null +++ b/core-api/src/test/java/com/absmartly/sdk/ContextSwitchbackClusterTest.java @@ -0,0 +1,524 @@ +package com.absmartly.sdk; + +import static com.absmartly.sdk.ContextSwitchbackTest.ANCHOR; +import static com.absmartly.sdk.ContextSwitchbackTest.HOUR; +import static com.absmartly.sdk.ContextSwitchbackTest.SEED_HI; +import static com.absmartly.sdk.ContextSwitchbackTest.SEED_LO; +import static com.absmartly.sdk.ContextSwitchbackTest.UID; +import static com.absmartly.sdk.ContextSwitchbackTest.UID_HASH; +import static com.absmartly.sdk.ContextSwitchbackTest.UNIT_TYPE; +import static com.absmartly.sdk.ContextSwitchbackTest.dataOf; +import static com.absmartly.sdk.ContextSwitchbackTest.switchback; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.math.BigDecimal; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import java8.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import com.absmartly.sdk.ContextSwitchbackTest.MutableClock; +import com.absmartly.sdk.internal.SwitchbackAssigner; +import com.absmartly.sdk.internal.SwitchbackBuckets; +import com.absmartly.sdk.internal.hashing.Hashing; +import com.absmartly.sdk.java.nio.charset.StandardCharsets; +import com.absmartly.sdk.json.ContextData; +import com.absmartly.sdk.json.Experiment; +import com.absmartly.sdk.json.Exposure; +import com.absmartly.sdk.json.PublishEvent; + +// Cluster bucket selection inside Context. Variants are derived from SwitchbackBuckets and +// SwitchbackAssigner, both pinned by the platform golden vectors. +class ContextSwitchbackClusterTest { + static final long SEED = ((long) SEED_HI << 32) | (SEED_LO & 0xffffffffL); + // 3 arms over 12 periods: two keys in different buckets are told apart by their schedules. + static final int ARMS = 3; + + MutableClock clock; + ContextDataProvider dataProvider; + ContextEventHandler eventHandler; + + @BeforeEach + void setUp() { + clock = new MutableClock(ANCHOR); + dataProvider = mock(ContextDataProvider.class); + eventHandler = mock(ContextEventHandler.class); + when(eventHandler.publish(any(), any())).thenReturn(CompletableFuture. completedFuture(null)); + } + + Context createContext(ContextData data) { + return createContext(TestUtils.mapOf(UNIT_TYPE, UID), data); + } + + Context createContext(Map units, ContextData data) { + final ContextConfig config = ContextConfig.create(); + config.setUnits(units); + return Context.create(clock, config, mock(ScheduledExecutorService.class), + CompletableFuture.completedFuture(data), dataProvider, eventHandler, null, + new DefaultVariableParser(), new AudienceMatcher(new DefaultAudienceDeserializer())); + } + + void refreshTo(Context context, Experiment experiment) { + when(dataProvider.getContextData()).thenReturn(CompletableFuture.completedFuture(dataOf(experiment))); + context.refresh(); + } + + PublishEvent publishEvent(Context context) { + context.publish(); + final ArgumentCaptor captor = ArgumentCaptor.forClass(PublishEvent.class); + verify(eventHandler, Mockito.timeout(5000).times(1)).publish(Mockito.eq(context), captor.capture()); + return captor.getValue(); + } + + Exposure[] publishExposures(Context context) { + return publishEvent(context).exposures; + } + + static Experiment attributeSwitchback(String path, int count) { + final Experiment experiment = switchback(ARMS, HOUR); + experiment.switchbackClusterAttribute = path; + experiment.switchbackClusterCount = count; + return experiment; + } + + static int expected(String key, int count, long periodIndex) { + final int bucket = SwitchbackBuckets.bucketOf(SEED_HI, SEED_LO, key, count); + return SwitchbackAssigner.assign(String.valueOf(bucket), periodIndex, SEED, ARMS); + } + + static int[] expectedSchedule(String key, int count) { + final int[] result = new int[12]; + for (int i = 0; i < result.length; ++i) { + result[i] = expected(key, count, i); + } + return result; + } + + int[] peekSchedule(Context context) { + final int[] result = new int[12]; + for (int i = 0; i < result.length; ++i) { + clock.now = ANCHOR + i * HOUR; + result[i] = context.peekTreatment("exp_sb"); + } + return result; + } + + static Exposure exposure(int variant, long exposedAt, boolean eligible, boolean audienceMismatch) { + return new Exposure(1, "exp_sb", UNIT_TYPE, variant, exposedAt, true, eligible, false, false, false, + audienceMismatch); + } + + @Test + void unitSourceHashesThePublishedUnitHashForEveryCount() { + for (final int count : new int[]{10, 50, 100}) { + final Experiment experiment = switchback(ARMS, HOUR); + experiment.switchbackClusterCount = count; + assertArrayEquals(expectedSchedule(UID_HASH, count), peekSchedule(createContext(dataOf(experiment)))); + } + } + + // A numeric UID is clustered by its canonical string (Hashing.unitToString, JavaScript toFixed(0)), + // the form ordinary assignment hashes and publish sends, so it shares the schedule, bucket and + // exposures of that string. + @Test + void numericUnitUidClustersLikeItsCanonicalString() { + final Object[][] cases = {{123456789L, "123456789"}, {123456789.0, "123456789"}, + {9007199254740993L, "9007199254740992"}, {125.75, "126"}}; + for (final int count : new int[]{10, 100}) { + final Experiment experiment = switchback(ARMS, HOUR); + experiment.switchbackClusterCount = count; + for (final Object[] pair : cases) { + final Context numeric = createContext(TestUtils.mapOf(UNIT_TYPE, pair[0]), dataOf(experiment)); + final PublishEvent numericEvent = exposeAndPublish(numeric); + final int[] numericSchedule = peekSchedule(numeric); + final Context text = createContext(TestUtils.mapOf(UNIT_TYPE, pair[1]), dataOf(experiment)); + final PublishEvent textEvent = exposeAndPublish(text); + + final String unitHash = new String(Hashing.hashUnit((String) pair[1]), StandardCharsets.US_ASCII); + assertArrayEquals(expectedSchedule(unitHash, count), numericSchedule); + assertArrayEquals(peekSchedule(text), numericSchedule); + assertArrayEquals(textEvent.units, numericEvent.units); + assertArrayEquals(textEvent.exposures, numericEvent.exposures); + assertEquals(unitHash, numericEvent.units[0].uid); + } + } + } + + // Independent reference (Python hashlib MD5 and murmur3 over the platform formulas) for numeric + // UIDs: the unit hash, its bucket and periods 0..11 of that bucket with 3 arms. + @Test + void numericUnitUidMatchesTheIndependentReference() { + final Object[][] cases = { + {123456789L, "JfnnlDI7RTiF9RgfG2JNCw", 10, 0, new int[]{2, 0, 1, 2, 1, 0, 0, 1, 2, 1, 0, 2}}, + {123456789L, "JfnnlDI7RTiF9RgfG2JNCw", 100, 10, new int[]{0, 2, 1, 2, 1, 0, 0, 2, 1, 1, 0, 2}}, + {9007199254740993L, "OngwCmjeKhIQyeNybDy4eg", 10, 7, new int[]{1, 2, 0, 0, 1, 2, 2, 1, 0, 0, 2, 1}}, + {9007199254740993L, "OngwCmjeKhIQyeNybDy4eg", 100, 37, + new int[]{0, 2, 1, 2, 0, 1, 2, 1, 0, 1, 2, 0}}}; + for (final Object[] golden : cases) { + final Experiment experiment = switchback(ARMS, HOUR); + experiment.switchbackClusterCount = (Integer) golden[2]; + final Context context = createContext(TestUtils.mapOf(UNIT_TYPE, golden[0]), dataOf(experiment)); + final int[] schedule = (int[]) golden[4]; + final PublishEvent event = exposeAndPublish(context); + + assertEquals(golden[1], event.units[0].uid); + assertEquals(golden[3], SwitchbackBuckets.bucketOf(SEED_HI, SEED_LO, event.units[0].uid, + (Integer) golden[2])); + assertArrayEquals(new Exposure[]{exposure(schedule[0], ANCHOR, true, false)}, event.exposures); + assertArrayEquals(schedule, peekSchedule(context)); + } + } + + PublishEvent exposeAndPublish(Context context) { + clock.now = ANCHOR; + context.getTreatment("exp_sb"); + return publishEvent(context); + } + + @Test + void attributeSourceHashesTheResolvedAttributeForEveryCount() { + for (final int count : new int[]{10, 50, 100}) { + clock.now = ANCHOR; + final Context context = createContext(dataOf(attributeSwitchback("city", count))); + context.setAttribute("city", "Lisboa"); + assertArrayEquals(expectedSchedule("Lisboa", count), peekSchedule(context)); + } + } + + @Test + void unitsSharingAnAttributeValueShareTheSchedule() { + final Context lisboa = createContext(dataOf(attributeSwitchback("city", 100))); + lisboa.setAttribute("city", "Lisboa"); + final Context porto = createContext(dataOf(attributeSwitchback("city", 100))); + porto.setAttribute("city", "Porto"); + final Context otherLisboa = createContext(TestUtils.mapOf(UNIT_TYPE, "another-store"), + dataOf(attributeSwitchback("city", 100))); + otherLisboa.setAttribute("city", "Lisboa"); + + assertArrayEquals(expectedSchedule("Lisboa", 100), peekSchedule(lisboa)); + assertArrayEquals(expectedSchedule("Lisboa", 100), peekSchedule(otherLisboa)); + assertArrayEquals(expectedSchedule("Porto", 100), peekSchedule(porto)); + assertNotEquals(SwitchbackBuckets.bucketOf(SEED_HI, SEED_LO, "Lisboa", 100), + SwitchbackBuckets.bucketOf(SEED_HI, SEED_LO, "Porto", 100)); + } + + @Test + void nestedAndArrayPathsAndIntegerValuesResolve() { + clock.now = ANCHOR; + final Context nested = createContext(dataOf(attributeSwitchback("geo/city", 50))); + nested.setAttribute("geo", TestUtils.mapOf("city", "Porto")); + assertArrayEquals(expectedSchedule("Porto", 50), peekSchedule(nested)); + + clock.now = ANCHOR; + final Context array = createContext(dataOf(attributeSwitchback("items/1/city", 50))); + array.setAttribute("items", TestUtils.listOf(TestUtils.mapOf("city", "Lisbon"), + TestUtils.mapOf("city", "Faro"))); + assertArrayEquals(expectedSchedule("Faro", 50), peekSchedule(array)); + + clock.now = ANCHOR; + final Context integer = createContext(dataOf(attributeSwitchback("region", 10))); + integer.setAttribute("region", 42L); + assertArrayEquals(expectedSchedule("42", 10), peekSchedule(integer)); + + // Values are read through their JSON encoding, as the Collector decodes them: an integral + // BigDecimal becomes an integer, a Character a string, and a Float a non-integral double. + clock.now = ANCHOR; + final Context decimal = createContext(dataOf(attributeSwitchback("geo/city", 50))); + decimal.setAttribute("geo", TestUtils.mapOf("city", new BigDecimal("42"))); + assertArrayEquals(expectedSchedule("42", 50), peekSchedule(decimal)); + + clock.now = ANCHOR; + final Context character = createContext(dataOf(attributeSwitchback("city", 100))); + character.setAttribute("city", 'c'); + assertArrayEquals(expectedSchedule("c", 100), peekSchedule(character)); + + clock.now = ANCHOR + 10; + final Context floating = createContext(dataOf(attributeSwitchback("city", 100))); + floating.setAttribute("city", 2.0f); + assertEquals(0, floating.getTreatment("exp_sb")); + assertArrayEquals(new Exposure[]{exposure(0, ANCHOR + 10, false, false)}, publishExposures(floating)); + } + + // Attribute values the Collector cannot bucket serve variant 0, not eligible, and never throw. + @Test + void unbucketableAttributeValuesAreIneligible() { + final Object[] values = {null, "", true, 1.5, 2.0, TestUtils.mapOf("a", "b"), TestUtils.listOf("x")}; + for (final Object value : values) { + final Context context = createContext(dataOf(attributeSwitchback("city", 10))); + context.setAttribute("city", value); + clock.now = ANCHOR + 10; + assertEquals(0, context.getTreatment("exp_sb"), String.valueOf(value)); + // Variant 0's own config, like any ineligible assignment. + assertEquals(0, context.getVariableValue("sb.arm", -1), String.valueOf(value)); + } + + final Context missing = createContext(dataOf(attributeSwitchback("geo/city", 10))); + missing.setAttribute("geo", TestUtils.mapOf("country", "PT")); + clock.now = ANCHOR + 10; + assertEquals(0, missing.getTreatment("exp_sb")); + assertArrayEquals(new Exposure[]{exposure(0, ANCHOR + 10, false, false)}, publishExposures(missing)); + } + + // A value with no JSON encoding has no cluster key. + @Test + void unencodableAttributeValuesAreIneligible() { + final Map cyclic = new HashMap(); + cyclic.put("self", cyclic); + final Object[] values = {cyclic, new Object()}; + for (final Object value : values) { + final Context context = createContext(dataOf(attributeSwitchback("geo/city", 10))); + context.setAttribute("geo", value); + assertEquals(0, context.getTreatment("exp_sb")); + } + } + + @Test + void unsupportedPathsAndDisallowedCountsAreIneligible() { + final Experiment[] experiments = {attributeSwitchback("items/0", 10), attributeSwitchback("items/-1/city", 10), + attributeSwitchback("", 10), attributeSwitchback("city", 1000), attributeSwitchback("city", 20), + attributeSwitchback("city", 0), switchback(ARMS, HOUR)}; + experiments[experiments.length - 1].switchbackClusterCount = null; + for (final Experiment experiment : experiments) { + final Context context = createContext(dataOf(experiment)); + context.setAttribute("city", "Lisboa"); + context.setAttribute("items", TestUtils.listOf(TestUtils.mapOf("city", "Lisbon"))); + for (int i = 0; i < 3; ++i) { + clock.now = ANCHOR + i * HOUR; + assertEquals(0, context.getTreatment("exp_sb"), experiment.toString()); + } + assertEquals(1, context.getPendingCount(), experiment.toString()); + } + } + + // The Collector reads the attribute set at or before the exposure instant, latest setAt first. + @Test + void latestAttributeSetAtOrBeforeTheResolutionInstantIsUsed() { + final Context context = createContext(dataOf(attributeSwitchback("city", 100))); + clock.now = ANCHOR + 2 * HOUR; + context.setAttribute("city", "Porto"); + clock.now = ANCHOR + 4 * HOUR; + context.setAttribute("city", "Lisboa"); + + clock.now = ANCHOR + HOUR; + assertEquals(0, context.peekTreatment("exp_sb")); + clock.now = ANCHOR + 3 * HOUR; + assertEquals(expected("Porto", 100, 3), context.peekTreatment("exp_sb")); + clock.now = ANCHOR + 4 * HOUR; + assertEquals(expected("Lisboa", 100, 4), context.peekTreatment("exp_sb")); + } + + // Within a period the bucket is fixed at first resolution; the next period reads + // the attribute again. + @Test + void setAttributeMidPeriodDoesNotReassignWithinThePeriod() { + final Context context = createContext(dataOf(attributeSwitchback("city", 100))); + clock.now = ANCHOR + 10; + context.setAttribute("city", "Lisboa"); + assertEquals(expected("Lisboa", 100, 0), context.getTreatment("exp_sb")); + + clock.now = ANCHOR + 20; + context.setAttribute("city", "Porto"); + assertEquals(expected("Lisboa", 100, 0), context.getTreatment("exp_sb")); + assertEquals(expected("Lisboa", 100, 0), context.peekTreatment("exp_sb")); + assertEquals(1, context.getPendingCount()); + + final int periods = 12; + int differing = 0; + for (int i = 1; i < periods; ++i) { + clock.now = ANCHOR + i * HOUR; + assertEquals(expected("Porto", 100, i), context.getTreatment("exp_sb")); + if (expected("Porto", 100, i) != expected("Lisboa", 100, i)) { + ++differing; + } + } + assertNotEquals(0, differing); + assertEquals(periods, context.getPendingCount()); + } + + // A unit first resolved without a key stays ineligible for the rest of that period. + @Test + void attributeSetAfterAnIneligibleResolutionWaitsForTheNextPeriod() { + final Context context = createContext(dataOf(attributeSwitchback("city", 100))); + clock.now = ANCHOR + 10; + assertEquals(0, context.getTreatment("exp_sb")); + context.setAttribute("city", "Lisboa"); + clock.now = ANCHOR + 20; + assertEquals(0, context.getTreatment("exp_sb")); + assertEquals(1, context.getPendingCount()); + + clock.now = ANCHOR + HOUR; + assertEquals(expected("Lisboa", 100, 1), context.getTreatment("exp_sb")); + assertArrayEquals(new Exposure[]{exposure(0, ANCHOR + 10, false, false), + exposure(expected("Lisboa", 100, 1), ANCHOR + HOUR, true, false)}, publishExposures(context)); + } + + // A slot replaced through an ineligible resolution still keeps the period's first key. + @Test + void trafficChangesWithinThePeriodKeepThePeriodsBucket() { + final Context context = createContext(dataOf(attributeSwitchback("city", 100))); + clock.now = ANCHOR + 10; + context.setAttribute("city", "Lisboa"); + assertEquals(expected("Lisboa", 100, 0), context.getTreatment("exp_sb")); + + clock.now = ANCHOR + 20; + context.setAttribute("city", "Porto"); + final Experiment noTraffic = attributeSwitchback("city", 100); + noTraffic.trafficSplit = new double[]{1.0, 0.0}; + refreshTo(context, noTraffic); + assertEquals(0, context.getTreatment("exp_sb")); + + refreshTo(context, attributeSwitchback("city", 100)); + assertEquals(expected("Lisboa", 100, 0), context.getTreatment("exp_sb")); + assertEquals(3, context.getPendingCount()); + + clock.now = ANCHOR + HOUR; + assertEquals(expected("Porto", 100, 1), context.getTreatment("exp_sb")); + } + + // A traffic miss never reads the key, so traffic restored later in the period reads it then. + @Test + void trafficRestoredWithinThePeriodReadsTheKey() { + final Experiment noTraffic = attributeSwitchback("city", 100); + noTraffic.trafficSplit = new double[]{1.0, 0.0}; + final Context context = createContext(dataOf(noTraffic)); + clock.now = ANCHOR + 10; + context.setAttribute("city", "Porto"); + assertEquals(0, context.getTreatment("exp_sb")); + + clock.now = ANCHOR + 20; + refreshTo(context, attributeSwitchback("city", 100)); + assertEquals(1, expected("Porto", 100, 0)); + assertEquals(expected("Porto", 100, 0), context.getTreatment("exp_sb")); + assertArrayEquals(new Exposure[]{exposure(0, ANCHOR + 10, false, false), + exposure(expected("Porto", 100, 0), ANCHOR + 20, true, false)}, publishExposures(context)); + } + + // An audience change re-resolves the period slot (new exposure state) but keeps its bucket. + @Test + void audienceRevalidationAfterSetAttributeKeepsThePeriodsBucket() { + final Experiment experiment = attributeSwitchback("city", 100); + experiment.audience = "{\"filter\":[{\"eq\":[{\"var\":\"tier\"},{\"value\":\"gold\"}]}]}"; + final Context context = createContext(dataOf(experiment)); + clock.now = ANCHOR + 10; + context.setAttribute("city", "Lisboa"); + context.setAttribute("tier", "gold"); + assertEquals(expected("Lisboa", 100, 0), context.getTreatment("exp_sb")); + + clock.now = ANCHOR + 20; + context.setAttribute("city", "Porto"); + context.setAttribute("tier", "silver"); + assertEquals(expected("Lisboa", 100, 0), context.getTreatment("exp_sb")); + + assertArrayEquals(new Exposure[]{exposure(expected("Lisboa", 100, 0), ANCHOR + 10, true, false), + exposure(expected("Lisboa", 100, 0), ANCHOR + 20, true, true)}, publishExposures(context)); + } + + // A slot first resolved without a key carries that missing key into its replacement. + @Test + void audienceRevalidationAfterAnIneligibleResolutionKeepsTheMissingKey() { + final Experiment experiment = attributeSwitchback("city", 100); + experiment.audience = "{\"filter\":[{\"eq\":[{\"var\":\"tier\"},{\"value\":\"gold\"}]}]}"; + final Context context = createContext(dataOf(experiment)); + clock.now = ANCHOR + 10; + context.setAttribute("tier", "gold"); + assertEquals(0, context.getTreatment("exp_sb")); + + clock.now = ANCHOR + 20; + context.setAttribute("city", "Porto"); + context.setAttribute("tier", "silver"); + assertEquals(1, expected("Porto", 100, 0)); + assertEquals(0, context.getTreatment("exp_sb")); + assertArrayEquals(new Exposure[]{exposure(0, ANCHOR + 10, false, false), + exposure(0, ANCHOR + 20, false, true)}, publishExposures(context)); + + clock.now = ANCHOR + HOUR; + assertEquals(expected("Porto", 100, 1), context.peekTreatment("exp_sb")); + // Period 1 of Porto is variant 0; period 2 is not, so the key is visibly read again. + clock.now = ANCHOR + 2 * HOUR; + assertEquals(2, expected("Porto", 100, 2)); + assertEquals(expected("Porto", 100, 2), context.peekTreatment("exp_sb")); + } + + @Test + void clusterCountOrAttributeChangesInvalidateThePeriod() { + final Context context = createContext(dataOf(attributeSwitchback("city", 100))); + clock.now = ANCHOR + 10; + context.setAttribute("city", "Lisboa"); + context.setAttribute("geo", TestUtils.mapOf("city", "Porto")); + assertEquals(expected("Lisboa", 100, 0), context.getTreatment("exp_sb")); + assertEquals(1, context.getPendingCount()); + + refreshTo(context, attributeSwitchback("city", 100)); + assertEquals(expected("Lisboa", 100, 0), context.getTreatment("exp_sb")); + assertEquals(1, context.getPendingCount()); + + refreshTo(context, attributeSwitchback("city", 10)); + assertEquals(expected("Lisboa", 10, 0), context.getTreatment("exp_sb")); + assertEquals(2, context.getPendingCount()); + + refreshTo(context, attributeSwitchback("geo/city", 10)); + assertEquals(expected("Porto", 10, 0), context.getTreatment("exp_sb")); + assertEquals(3, context.getPendingCount()); + + final Experiment unitSource = switchback(ARMS, HOUR); + unitSource.switchbackClusterCount = 10; + refreshTo(context, unitSource); + assertEquals(expected(UID_HASH, 10, 0), context.getTreatment("exp_sb")); + assertEquals(4, context.getPendingCount()); + } + + // With no cluster unit type, the experiment's unitType alone decides the hashed unit, so a + // unitType change re-keys the period. + @Test + void unitTypeChangeWithoutClusterUnitTypeRekeysThePeriod() { + final String userUid = "user-0002"; + final String userHash = new String(Hashing.hashUnit(userUid), StandardCharsets.US_ASCII); + final Experiment byStore = switchback(ARMS, HOUR); + byStore.switchbackClusterUnitType = null; + final Context context = createContext(TestUtils.mapOf(UNIT_TYPE, UID, "user_id", userUid), dataOf(byStore)); + assertNotEquals(expected(UID_HASH, 100, 0), expected(userHash, 100, 0)); + + clock.now = ANCHOR + 1; + assertEquals(expected(UID_HASH, 100, 0), context.getTreatment("exp_sb")); + + final Experiment byUser = switchback(ARMS, HOUR); + byUser.switchbackClusterUnitType = null; + byUser.unitType = "user_id"; + refreshTo(context, byUser); + assertEquals(expected(userHash, 100, 0), context.getTreatment("exp_sb")); + } + + @Test + void customAssignmentAppliesOnlyToUnitsWithAClusterKey() { + final Context keyed = createContext(dataOf(attributeSwitchback("city", 10))); + keyed.setAttribute("city", "Lisboa"); + keyed.setCustomAssignment("exp_sb", 2); + assertEquals(2, keyed.getTreatment("exp_sb")); + + final Context unkeyed = createContext(dataOf(attributeSwitchback("city", 10))); + unkeyed.setCustomAssignment("exp_sb", 2); + assertEquals(0, unkeyed.getTreatment("exp_sb")); + } + + @Test + void unitExperimentsIgnoreClusterFields() { + final Experiment unit = attributeSwitchback("city", 1000); + unit.randomizationType = null; + final Experiment reference = switchback(ARMS, HOUR); + reference.randomizationType = null; + final Context withFields = createContext(dataOf(unit)); + final Context without = createContext(dataOf(reference)); + assertEquals(without.getTreatment("exp_sb"), withFields.getTreatment("exp_sb")); + } +} diff --git a/core-api/src/test/java/com/absmartly/sdk/ContextSwitchbackTest.java b/core-api/src/test/java/com/absmartly/sdk/ContextSwitchbackTest.java new file mode 100644 index 0000000..b4e4bd0 --- /dev/null +++ b/core-api/src/test/java/com/absmartly/sdk/ContextSwitchbackTest.java @@ -0,0 +1,1113 @@ +package com.absmartly.sdk; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CyclicBarrier; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java8.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +import com.absmartly.sdk.internal.SwitchbackAssigner; +import com.absmartly.sdk.internal.SwitchbackBuckets; +import com.absmartly.sdk.internal.hashing.Hashing; +import com.absmartly.sdk.java.nio.charset.StandardCharsets; +import com.absmartly.sdk.java.time.Clock; +import com.absmartly.sdk.json.ContextData; +import com.absmartly.sdk.json.Experiment; +import com.absmartly.sdk.json.ExperimentApplication; +import com.absmartly.sdk.json.ExperimentVariant; +import com.absmartly.sdk.json.Exposure; +import com.absmartly.sdk.json.PublishEvent; + +class ContextSwitchbackTest { + static final String UNIT_TYPE = "store_id"; + static final String UID = "e791e240fcd3df7d238cfc285f475e8152fcc0ec"; + // Hashing.hashUnit(UID): the MD5/base64url string the Collector's UnitHasher also produces. + static final String UID_HASH = "pAE3a1i5Drs5mKRNq56adA"; + + static final long HOUR = 3_600_000L; + static final long DAY = 24 * HOUR; + // 2026-01-01T00:00:00Z, hour aligned. + static final long ANCHOR = 1_767_225_600_000L; + + static final int SEED_HI = -5; + static final int SEED_LO = -7; + + // UID_HASH falls in bucket 35 of 100 under seed (SEED_HI, SEED_LO). Periods 0..11 of bucket + // "35", computed with an independent Python reference (hashlib MD5 and murmur3 over the + // platform formulas) that reproduces every bucket and schedule golden vector. + static final int CLUSTER_COUNT = 100; + static final int UID_BUCKET = 35; + static final int[] EXPECTED_2_ARMS = {0, 1, 1, 0, 0, 1, 0, 1, 0, 1, 1, 0}; + static final int[] EXPECTED_3_ARMS = {0, 2, 1, 2, 0, 1, 0, 1, 2, 2, 1, 0}; + static final int[] EXPECTED_10_ARMS = {5, 3, 8, 9, 0, 7, 4, 6, 1, 2, 2, 4}; + // Same reference fed UID_HASH itself rather than its bucket: what per-value clustering serves. + static final int[] PER_VALUE_3_ARMS = {1, 2, 0, 2, 1, 0, 2, 0, 1, 1, 2, 0}; + + static final class MutableClock extends Clock { + long now; + // When set, the next read returns this value once, then reads fall back to `now`. + Long nextRead; + + MutableClock(long now) { + this.now = now; + } + + @Override + public long millis() { + if (nextRead != null) { + final long value = nextRead; + nextRead = null; + return value; + } + return now; + } + } + + MutableClock clock; + ContextDataProvider dataProvider; + ContextEventHandler eventHandler; + ScheduledExecutorService scheduler; + + @BeforeEach + void setUp() { + clock = new MutableClock(ANCHOR); + dataProvider = mock(ContextDataProvider.class); + eventHandler = mock(ContextEventHandler.class); + scheduler = mock(ScheduledExecutorService.class); + when(eventHandler.publish(any(), any())).thenReturn(CompletableFuture. completedFuture(null)); + } + + static Experiment switchback(int id, String name, int armCount, long periodMs) { + final Experiment experiment = new Experiment(); + experiment.id = id; + experiment.name = name; + experiment.unitType = UNIT_TYPE; + experiment.iteration = 1; + experiment.seedHi = SEED_HI; + experiment.seedLo = SEED_LO; + experiment.split = new double[armCount]; + experiment.variants = new ExperimentVariant[armCount]; + for (int i = 0; i < armCount; ++i) { + experiment.split[i] = 1.0 / armCount; + experiment.variants[i] = new ExperimentVariant(String.valueOf((char) ('A' + i)), + "{\"sb.arm\":" + i + "}"); + } + experiment.trafficSeedHi = 1; + experiment.trafficSeedLo = 2; + experiment.trafficSplit = new double[]{0.0, 1.0}; + experiment.applications = new ExperimentApplication[]{new ExperimentApplication("website")}; + experiment.randomizationType = "switchback"; + experiment.switchbackClusterUnitType = UNIT_TYPE; + experiment.switchbackStartAt = ANCHOR; + experiment.switchbackPeriodMs = periodMs; + experiment.switchbackCarryoverDurationMs = 0L; + experiment.switchbackBurnInDurationMs = 0L; + experiment.switchbackBurnOutDurationMs = 0L; + experiment.switchbackDesignType = "balanced_block"; + experiment.switchbackEstimand = "global_average_treatment_effect"; + experiment.switchbackClusterCount = CLUSTER_COUNT; + return experiment; + } + + static Experiment switchback(int armCount, long periodMs) { + return switchback(1, "exp_sb", armCount, periodMs); + } + + static ContextData dataOf(Experiment... experiments) { + final ContextData data = new ContextData(); + data.experiments = experiments; + return data; + } + + Context createContext(Map units, ContextData data) { + return createContext(units, data, new AudienceMatcher(new DefaultAudienceDeserializer())); + } + + Context createContext(Map units, ContextData data, AudienceMatcher audienceMatcher) { + final ContextConfig config = ContextConfig.create(); + if (units != null) { + config.setUnits(units); + } + return Context.create(clock, config, scheduler, CompletableFuture.completedFuture(data), dataProvider, + eventHandler, null, new DefaultVariableParser(), audienceMatcher); + } + + Context createContext(ContextData data) { + return createContext(TestUtils.mapOf(UNIT_TYPE, UID), data); + } + + List publishAndCapture(Context context, int expectedPublishes) { + context.publish(); + final ArgumentCaptor captor = ArgumentCaptor.forClass(PublishEvent.class); + verify(eventHandler, Mockito.timeout(5000).times(expectedPublishes)).publish(Mockito.eq(context), + captor.capture()); + return captor.getAllValues(); + } + + Exposure[] publishExposures(Context context) { + final List events = publishAndCapture(context, 1); + final Exposure[] exposures = events.get(0).exposures; + return (exposures != null) ? exposures : new Exposure[0]; + } + + static Exposure exposure(int id, String name, int variant, long exposedAt, boolean assigned, boolean eligible) { + return new Exposure(id, name, UNIT_TYPE, variant, exposedAt, assigned, eligible, false, false, false, false); + } + + int[] peekSchedule(Context context, String name, long periodMs, int periods) { + final int[] result = new int[periods]; + for (int i = 0; i < periods; ++i) { + clock.now = ANCHOR + i * periodMs; + result[i] = context.peekTreatment(name); + } + return result; + } + + void refreshTo(Context context, Experiment... experiments) { + when(dataProvider.getContextData()).thenReturn(CompletableFuture.completedFuture(dataOf(experiments))); + context.refresh(); + } + + // Holdout 11 for UID under seed (13, 111); split decides whether UID is held out. + static Experiment holdout(double[] split) { + final Experiment holdout = new Experiment(); + holdout.id = 11; + holdout.name = "holdout_a"; + holdout.unitType = UNIT_TYPE; + holdout.iteration = 1; + holdout.seedHi = 13; + holdout.seedLo = 111; + holdout.split = split; + holdout.trafficSplit = new double[]{0.0, 1.0}; + return holdout; + } + + @Test + void clusterIdentityIsTheSdkUnitHash() { + assertEquals(UID_HASH, new String(Hashing.hashUnit(UID), StandardCharsets.US_ASCII)); + + final Context context = createContext(dataOf(switchback(3, HOUR))); + final int[] served = peekSchedule(context, "exp_sb", HOUR, 12); + + assertArrayEquals(EXPECTED_3_ARMS, served); + assertFalse(Arrays.equals(PER_VALUE_3_ARMS, served)); + assertEquals(UID_BUCKET, SwitchbackBuckets.bucketOf(SEED_HI, SEED_LO, UID_HASH, CLUSTER_COUNT)); + for (int i = 0; i < 12; ++i) { + assertEquals(SwitchbackAssigner.assign(String.valueOf(UID_BUCKET), i, + ((long) SEED_HI << 32) | (SEED_LO & 0xffffffffL), 3), served[i]); + } + } + + @Test + void servesTwoThreeAndTenArmPermutations() { + final Context context = createContext(dataOf(switchback(1, "exp_2", 2, HOUR), + switchback(2, "exp_3", 3, HOUR), switchback(3, "exp_10", 10, HOUR))); + + assertArrayEquals(EXPECTED_2_ARMS, peekSchedule(context, "exp_2", HOUR, 12)); + assertArrayEquals(EXPECTED_3_ARMS, peekSchedule(context, "exp_3", HOUR, 12)); + assertArrayEquals(EXPECTED_10_ARMS, peekSchedule(context, "exp_10", HOUR, 12)); + } + + @Test + void oneHourBoundaryAtExactEdgeMilliseconds() { + final Context context = createContext(dataOf(switchback(2, HOUR))); + + clock.now = ANCHOR - 1; + assertEquals(0, context.peekTreatment("exp_sb")); + clock.now = ANCHOR; + assertEquals(EXPECTED_2_ARMS[0], context.peekTreatment("exp_sb")); + clock.now = ANCHOR + HOUR - 1; + assertEquals(EXPECTED_2_ARMS[0], context.peekTreatment("exp_sb")); + clock.now = ANCHOR + HOUR; + assertEquals(EXPECTED_2_ARMS[1], context.peekTreatment("exp_sb")); + clock.now = ANCHOR + 3 * HOUR - 1; + assertEquals(EXPECTED_2_ARMS[2], context.peekTreatment("exp_sb")); + clock.now = ANCHOR + 3 * HOUR; + assertEquals(EXPECTED_2_ARMS[3], context.peekTreatment("exp_sb")); + } + + @Test + void twentyFourHourBoundaryAtExactEdgeMilliseconds() { + final Context context = createContext(dataOf(switchback(2, DAY))); + + clock.now = ANCHOR - 1; + assertEquals(0, context.peekTreatment("exp_sb")); + clock.now = ANCHOR; + assertEquals(EXPECTED_2_ARMS[0], context.peekTreatment("exp_sb")); + clock.now = ANCHOR + HOUR; + assertEquals(EXPECTED_2_ARMS[0], context.peekTreatment("exp_sb")); + clock.now = ANCHOR + DAY - 1; + assertEquals(EXPECTED_2_ARMS[0], context.peekTreatment("exp_sb")); + clock.now = ANCHOR + DAY; + assertEquals(EXPECTED_2_ARMS[1], context.peekTreatment("exp_sb")); + clock.now = ANCHOR + 4 * DAY - 1; + assertEquals(EXPECTED_2_ARMS[3], context.peekTreatment("exp_sb")); + clock.now = ANCHOR + 4 * DAY; + assertEquals(EXPECTED_2_ARMS[4], context.peekTreatment("exp_sb")); + } + + // The anchor is the start instant rounded up to the hour; start..anchor is pre-period. + @Test + void unalignedStartIsRoundedUpAndPreAnchorIsIneligible() { + final Experiment experiment = switchback(2, HOUR); + final long start = ANCHOR - 1_234_567L; + experiment.switchbackStartAt = start; + final Context context = createContext(dataOf(experiment)); + + clock.now = start; + assertEquals(0, context.getTreatment("exp_sb")); + clock.now = ANCHOR - 1; + assertEquals(0, context.getTreatment("exp_sb")); + clock.now = ANCHOR; + assertEquals(EXPECTED_2_ARMS[0], context.getTreatment("exp_sb")); + clock.now = ANCHOR + HOUR; + assertEquals(EXPECTED_2_ARMS[1], context.getTreatment("exp_sb")); + + final Exposure[] exposures = publishExposures(context); + assertArrayEquals(new Exposure[]{ + exposure(1, "exp_sb", 0, start, true, false), + exposure(1, "exp_sb", EXPECTED_2_ARMS[0], ANCHOR, true, true), + exposure(1, "exp_sb", EXPECTED_2_ARMS[1], ANCHOR + HOUR, true, true), + }, exposures); + } + + @Test + void nullStartAtIsIneligibleAndNeverFallsBackToUnitAssignment() { + final Experiment experiment = switchback(2, HOUR); + experiment.switchbackStartAt = null; + final Context context = createContext(dataOf(experiment)); + + for (int i = 0; i < 4; ++i) { + clock.now = ANCHOR + i * HOUR; + assertEquals(0, context.getTreatment("exp_sb")); + } + + assertArrayEquals(new Exposure[]{exposure(1, "exp_sb", 0, ANCHOR, true, false)}, publishExposures(context)); + } + + @Test + void samePeriodIsCachedAndNextPeriodRollsOverWithOneExposureEach() { + final Context context = createContext(dataOf(switchback(2, HOUR))); + + clock.now = ANCHOR + 10; + assertEquals(EXPECTED_2_ARMS[0], context.getTreatment("exp_sb")); + assertEquals(1, context.getPendingCount()); + clock.now = ANCHOR + HOUR - 1; + assertEquals(EXPECTED_2_ARMS[0], context.getTreatment("exp_sb")); + assertEquals(EXPECTED_2_ARMS[0], context.peekTreatment("exp_sb")); + assertEquals(1, context.getPendingCount()); + + clock.now = ANCHOR + HOUR; + assertEquals(EXPECTED_2_ARMS[1], context.peekTreatment("exp_sb")); + assertEquals(1, context.getPendingCount()); + assertEquals(EXPECTED_2_ARMS[1], context.getTreatment("exp_sb")); + assertEquals(2, context.getPendingCount()); + clock.now = ANCHOR + HOUR + 30 * 60_000L; + assertEquals(EXPECTED_2_ARMS[1], context.getTreatment("exp_sb")); + assertEquals(2, context.getPendingCount()); + + final Exposure[] exposures = publishExposures(context); + assertArrayEquals(new Exposure[]{ + exposure(1, "exp_sb", EXPECTED_2_ARMS[0], ANCHOR + 10, true, true), + exposure(1, "exp_sb", EXPECTED_2_ARMS[1], ANCHOR + HOUR, true, true), + }, exposures); + } + + @Test + void variableLookupsFollowThePeriodAndExposeOncePerPeriod() { + final Context context = createContext(dataOf(switchback(3, HOUR))); + + assertEquals(TestUtils.mapOf("sb.arm", TestUtils.listOf("exp_sb")), context.getVariableKeys()); + assertArrayEquals(new String[]{"exp_sb"}, context.getExperiments()); + + for (int i = 0; i < 3; ++i) { + clock.now = ANCHOR + i * HOUR; + assertEquals(EXPECTED_3_ARMS[i], context.peekVariableValue("sb.arm", -1)); + assertEquals(EXPECTED_3_ARMS[i], context.getVariableValue("sb.arm", -1)); + assertEquals(EXPECTED_3_ARMS[i], context.getVariableValue("sb.arm", -1)); + assertEquals(EXPECTED_3_ARMS[i], context.getTreatment("exp_sb")); + } + assertEquals(3, context.getPendingCount()); + } + + @Test + void missingClusterUnitIsUnassignedWithoutFallback() { + final Context context = createContext(TestUtils.mapOf("user_id", UID), dataOf(switchback(2, HOUR))); + + for (int i = 0; i < 3; ++i) { + clock.now = ANCHOR + i * HOUR; + assertEquals(0, context.peekTreatment("exp_sb")); + assertEquals(0, context.getTreatment("exp_sb")); + assertEquals(-1, context.getVariableValue("sb.arm", -1)); + } + + for (final Exposure exposure : publishExposures(context)) { + assertEquals(0, exposure.variant); + assertFalse(exposure.assigned); + } + } + + @Test + void fullOnServesTheFullOnVariantInEveryPeriod() { + final Experiment experiment = switchback(3, HOUR); + experiment.fullOnVariant = 2; + final Context context = createContext(dataOf(experiment)); + + for (int i = 0; i < 4; ++i) { + clock.now = ANCHOR + i * HOUR; + assertEquals(2, context.getTreatment("exp_sb")); + } + for (final Exposure exposure : publishExposures(context)) { + assertEquals(2, exposure.variant); + assertTrue(exposure.fullOn); + } + } + + @Test + void overrideAndCustomAssignmentWinInEveryPeriod() { + final Context context = createContext(dataOf(switchback(1, "exp_override", 3, HOUR), + switchback(2, "exp_custom", 3, HOUR))); + context.setOverride("exp_override", 7); + context.setCustomAssignment("exp_custom", 2); + + for (int i = 0; i < 3; ++i) { + clock.now = ANCHOR + i * HOUR; + assertEquals(7, context.getTreatment("exp_override")); + assertEquals(2, context.getTreatment("exp_custom")); + } + for (final Exposure exposure : publishExposures(context)) { + if (exposure.name.equals("exp_override")) { + assertTrue(exposure.overridden); + assertEquals(7, exposure.variant); + } else { + assertTrue(exposure.custom); + assertEquals(2, exposure.variant); + } + } + } + + // Traffic eligibility uses the existing per-unit traffic hash, so it matches a unit-randomized + // twin with the same traffic seed and stays fixed across periods. + @Test + void trafficEligibilityIsClusterSticky() { + int eligibleCount = 0; + int ineligibleCount = 0; + for (int u = 0; u < 24; ++u) { + reset(eventHandler); + when(eventHandler.publish(any(), any())).thenReturn(CompletableFuture. completedFuture(null)); + + final Experiment sb = switchback(1, "exp_sb", 2, HOUR); + sb.trafficSplit = new double[]{0.5, 0.5}; + final Experiment twin = switchback(2, "exp_unit", 2, HOUR); + twin.trafficSplit = new double[]{0.5, 0.5}; + twin.randomizationType = null; + twin.switchbackStartAt = null; + + final Context context = createContext(TestUtils.mapOf(UNIT_TYPE, "store_" + u), dataOf(sb, twin)); + clock.now = ANCHOR; + context.getTreatment("exp_unit"); + for (int i = 0; i < 4; ++i) { + clock.now = ANCHOR + i * HOUR; + context.getTreatment("exp_sb"); + } + + final Exposure[] exposures = publishExposures(context); + assertEquals(5, exposures.length); + final boolean twinEligible = exposures[0].eligible; + for (int i = 1; i < exposures.length; ++i) { + assertEquals("exp_sb", exposures[i].name); + assertEquals(twinEligible, exposures[i].eligible); + if (!twinEligible) { + assertEquals(0, exposures[i].variant); + } + } + if (twinEligible) { + ++eligibleCount; + } else { + ++ineligibleCount; + } + } + assertTrue(eligibleCount > 0); + assertTrue(ineligibleCount > 0); + } + + @Test + void unitExperimentsAreUnaffected() { + final Experiment unit = switchback(2, HOUR); + unit.randomizationType = "unit"; + final Experiment absent = switchback(2, "exp_absent", 2, HOUR); + absent.randomizationType = null; + + final Context context = createContext(dataOf(unit, absent)); + final int variant = context.getTreatment("exp_sb"); + final int absentVariant = context.getTreatment("exp_absent"); + for (int i = 1; i < 6; ++i) { + clock.now = ANCHOR + i * HOUR; + assertEquals(variant, context.getTreatment("exp_sb")); + assertEquals(absentVariant, context.getTreatment("exp_absent")); + } + assertEquals(2, context.getPendingCount()); + } + + @Test + void refreshInvalidatesWhenGridFieldsChangeButNotForAnalysisOnlyFields() { + final Context context = createContext(dataOf(switchback(2, HOUR))); + clock.now = ANCHOR + 10; + assertEquals(EXPECTED_2_ARMS[0], context.getTreatment("exp_sb")); + assertEquals(1, context.getPendingCount()); + + final Experiment burnsChanged = switchback(2, HOUR); + burnsChanged.switchbackBurnInDurationMs = 600_000L; + burnsChanged.switchbackBurnOutDurationMs = 300_000L; + burnsChanged.switchbackCarryoverDurationMs = 900_000L; + burnsChanged.switchbackEstimand = "other"; + refreshTo(context, burnsChanged); + assertEquals(EXPECTED_2_ARMS[0], context.getTreatment("exp_sb")); + assertEquals(1, context.getPendingCount()); + + // Same period index 0 under both grids, so only the config comparison can invalidate. + final Experiment periodChanged = switchback(2, 2 * HOUR); + refreshTo(context, periodChanged); + assertEquals(EXPECTED_2_ARMS[0], context.getTreatment("exp_sb")); + assertEquals(2, context.getPendingCount()); + + final Experiment designChanged = switchback(2, 2 * HOUR); + designChanged.switchbackDesignType = "other_design"; + refreshTo(context, designChanged); + context.getTreatment("exp_sb"); + assertEquals(3, context.getPendingCount()); + + final Experiment toUnit = switchback(2, 2 * HOUR); + toUnit.randomizationType = "unit"; + refreshTo(context, toUnit); + context.getTreatment("exp_sb"); + assertEquals(4, context.getPendingCount()); + + final Experiment backToSwitchback = switchback(2, 2 * HOUR); + refreshTo(context, backToSwitchback); + context.getTreatment("exp_sb"); + assertEquals(5, context.getPendingCount()); + } + + @Test + void refreshSettingStartAtStartsTheGrid() { + final Experiment pending = switchback(2, HOUR); + pending.switchbackStartAt = null; + final Context context = createContext(dataOf(pending)); + clock.now = ANCHOR + 10; + assertEquals(0, context.getTreatment("exp_sb")); + + refreshTo(context, switchback(2, HOUR)); + assertEquals(EXPECTED_2_ARMS[0], context.getTreatment("exp_sb")); + assertEquals(2, context.getPendingCount()); + } + + @Test + void holdoutSuppressionAndExposureSpanPeriods() { + // split[0.1,0.9] -> variant 0 (held out) for UID. + final Experiment holdout = holdout(new double[]{0.1, 0.9}); + + final Experiment covered = switchback(2, HOUR); + covered.holdoutIds = new int[]{11}; + final ContextData data = dataOf(covered); + data.holdouts = new Experiment[]{holdout}; + + final Context context = createContext(data); + for (int i = 0; i < 3; ++i) { + clock.now = ANCHOR + i * HOUR; + assertEquals(0, context.getTreatment("exp_sb")); + } + + assertArrayEquals(new Exposure[]{ + new Exposure(11, "holdout_a", UNIT_TYPE, 0, ANCHOR, true, true, false, false, false, false) + }, publishExposures(context)); + } + + @Test + void customFieldValuesAndUnitLookupsWork() { + final Context context = createContext(dataOf(switchback(2, HOUR))); + assertEquals(UID, context.getUnit(UNIT_TYPE)); + assertEquals(0, context.getCustomFieldKeys().length); + assertEquals(null, context.getCustomFieldValue("exp_sb", "missing")); + } + + // Each experiment keeps entries for at most its two most recent periods. + @Test + void perPeriodCacheIsBounded() throws Exception { + final Context context = createContext(dataOf(switchback(2, HOUR))); + for (int i = 0; i < 50; ++i) { + clock.now = ANCHOR + i * HOUR; + context.getTreatment("exp_sb"); + } + final Field field = Context.class.getDeclaredField("assignmentCache_"); + field.setAccessible(true); + assertEquals(2, ((Map) field.get(context)).size()); + assertEquals(50, context.getPendingCount()); + } + + @Test + void clusterUnitTypeDifferentFromUnitTypeIsIneligible() { + final Experiment experiment = switchback(2, HOUR); + experiment.switchbackClusterUnitType = "user_id"; + final Context context = createContext( + TestUtils.mapOf(UNIT_TYPE, UID, "user_id", "u1"), dataOf(experiment)); + for (int i = 0; i < 3; ++i) { + clock.now = ANCHOR + i * HOUR; + assertEquals(0, context.getTreatment("exp_sb")); + } + final Exposure[] exposures = publishExposures(context); + assertEquals(1, exposures.length); + assertFalse(exposures[0].eligible); + } + + // The publish payload carries no switchback-specific exposure fields. + @Test + void publishPayloadHasNoNewFields() { + final Context context = createContext(dataOf(switchback(2, HOUR))); + clock.now = ANCHOR + 5; + context.getTreatment("exp_sb"); + clock.now = ANCHOR + HOUR + 7; + context.getTreatment("exp_sb"); + clock.now = ANCHOR + HOUR + 9; + + final PublishEvent event = publishAndCapture(context, 1).get(0); + final String json = new String(new DefaultContextEventSerializer().serialize(event), + StandardCharsets.UTF_8); + + assertEquals("{\"hashed\":true,\"units\":[{\"type\":\"store_id\",\"uid\":\"" + UID_HASH + "\"}]," + + "\"publishedAt\":" + (ANCHOR + HOUR + 9) + "," + + "\"exposures\":[" + + "{\"id\":1,\"name\":\"exp_sb\",\"unit\":\"store_id\",\"variant\":" + EXPECTED_2_ARMS[0] + + ",\"exposedAt\":" + (ANCHOR + 5) + + ",\"assigned\":true,\"eligible\":true,\"overridden\":false,\"fullOn\":false,\"custom\":false," + + "\"audienceMismatch\":false}," + + "{\"id\":1,\"name\":\"exp_sb\",\"unit\":\"store_id\",\"variant\":" + EXPECTED_2_ARMS[1] + + ",\"exposedAt\":" + (ANCHOR + HOUR + 7) + + ",\"assigned\":true,\"eligible\":true,\"overridden\":false,\"fullOn\":false,\"custom\":false," + + "\"audienceMismatch\":false}" + + "]}", json); + assertNotEquals(EXPECTED_2_ARMS[0], EXPECTED_2_ARMS[1]); + } + + @Test + void periodsPublishedAcrossFlushesStayOnePerPeriod() { + final Context context = createContext(dataOf(switchback(2, HOUR))); + final List variants = new ArrayList(); + clock.now = ANCHOR; + context.getTreatment("exp_sb"); + context.publish(); + clock.now = ANCHOR + 1; + context.getTreatment("exp_sb"); + assertEquals(0, context.getPendingCount()); + clock.now = ANCHOR + HOUR; + context.getTreatment("exp_sb"); + context.publish(); + + for (final PublishEvent event : publishAndCapture(context, 2)) { + for (final Exposure exposure : event.exposures) { + variants.add(exposure.variant); + } + } + assertEquals(TestUtils.listOf(EXPECTED_2_ARMS[0], EXPECTED_2_ARMS[1]), variants); + verify(eventHandler, times(2)).publish(any(), any()); + } + + // The clock crosses into period 1 between resolution (first read) and any later read; the + // exposure must still carry the instant that selected period 0. + @Test + void treatmentExposureIsStampedWithTheResolutionInstant() { + final Context context = createContext(dataOf(switchback(2, HOUR))); + clock.now = ANCHOR + HOUR; + clock.nextRead = ANCHOR + HOUR - 1; + + assertEquals(EXPECTED_2_ARMS[0], context.getTreatment("exp_sb")); + assertArrayEquals(new Exposure[]{exposure(1, "exp_sb", EXPECTED_2_ARMS[0], ANCHOR + HOUR - 1, true, true)}, + publishExposures(context)); + } + + @Test + void variableExposureIsStampedWithTheResolutionInstantAcrossHoldoutTriggering() { + // Two arms, all traffic in arm 1: never holds the covered experiment out. + final Experiment holdout = holdout(new double[]{0.0, 1.0}); + + final Experiment covered = switchback(2, HOUR); + covered.holdoutIds = new int[]{11}; + final ContextData data = dataOf(covered); + data.holdouts = new Experiment[]{holdout}; + + final Context context = createContext(data); + clock.now = ANCHOR + HOUR; + clock.nextRead = ANCHOR + HOUR - 1; + + assertEquals(EXPECTED_2_ARMS[0], context.getVariableValue("sb.arm", -1)); + final Exposure[] exposures = publishExposures(context); + assertEquals(2, exposures.length); + // Holdout membership is not period-scoped and keeps its own enqueue-time reading. + assertEquals(new Exposure(11, "holdout_a", UNIT_TYPE, 1, ANCHOR + HOUR, true, true, false, false, false, + false), exposures[0]); + assertEquals(exposure(1, "exp_sb", EXPECTED_2_ARMS[0], ANCHOR + HOUR - 1, true, true), exposures[1]); + } + + // A peek caches the period's assignment without exposing; the later getTreatment exposes at + // its own resolution instant, not the peek's. + @Test + void exposureAfterPeekUsesTheExposingCallsInstant() { + final Context context = createContext(dataOf(switchback(2, HOUR))); + clock.now = ANCHOR + 10; + assertEquals(EXPECTED_2_ARMS[0], context.peekTreatment("exp_sb")); + clock.now = ANCHOR + 20 * 60_000L; + assertEquals(EXPECTED_2_ARMS[0], context.getTreatment("exp_sb")); + assertArrayEquals( + new Exposure[]{exposure(1, "exp_sb", EXPECTED_2_ARMS[0], ANCHOR + 20 * 60_000L, true, true)}, + publishExposures(context)); + } + + // Unit experiments keep stamping at enqueue time. + @Test + void unitExposureTimingIsUnchanged() { + final Experiment unit = switchback(2, HOUR); + unit.randomizationType = "unit"; + final Context context = createContext(dataOf(unit)); + clock.now = ANCHOR + HOUR; + clock.nextRead = ANCHOR + HOUR - 1; + + final int variant = context.getTreatment("exp_sb"); + assertArrayEquals(new Exposure[]{exposure(1, "exp_sb", variant, ANCHOR + HOUR, true, true)}, + publishExposures(context)); + } + + // A grid change that lowers the current index below the retained ones must not evict the + // entry it just installed. + @Test + void gridRefreshToALowerIndexExposesOnce() { + final Context context = createContext(dataOf(switchback(2, HOUR))); + clock.now = ANCHOR + 99 * HOUR; + context.getTreatment("exp_sb"); + clock.now = ANCHOR + 100 * HOUR; + context.getTreatment("exp_sb"); + assertEquals(2, context.getPendingCount()); + + final Experiment restarted = switchback(2, HOUR); + restarted.switchbackStartAt = ANCHOR + 100 * HOUR; + refreshTo(context, restarted); + for (int i = 0; i < 3; ++i) { + assertEquals(EXPECTED_2_ARMS[0], context.getTreatment("exp_sb")); + } + assertEquals(3, context.getPendingCount()); + } + + @Test + void periodLengthRefreshToALowerIndexExposesOnce() { + final Context context = createContext(dataOf(switchback(2, HOUR))); + clock.now = ANCHOR + 100 * HOUR; + context.getTreatment("exp_sb"); + clock.now = ANCHOR + 101 * HOUR; + context.getTreatment("exp_sb"); + + refreshTo(context, switchback(2, 200 * HOUR)); + context.getTreatment("exp_sb"); + context.getTreatment("exp_sb"); + assertEquals(3, context.getPendingCount()); + } + + // Rewinding the clock across retained periods re-exposes each re-entered evicted period at most + // once, never once per call. + @Test + void clockRewindReexposesAnEvictedPeriodAtMostOnce() { + final Context context = createContext(dataOf(switchback(2, HOUR))); + for (int i = 0; i < 3; ++i) { + clock.now = ANCHOR + i * HOUR; + context.getTreatment("exp_sb"); + } + assertEquals(3, context.getPendingCount()); + + clock.now = ANCHOR; + for (int i = 0; i < 3; ++i) { + assertEquals(EXPECTED_2_ARMS[0], context.getTreatment("exp_sb")); + } + assertEquals(4, context.getPendingCount()); + + // Retained {0, 2}: period 2 is still deduplicated, period 1 was evicted. + clock.now = ANCHOR + 2 * HOUR; + context.getTreatment("exp_sb"); + assertEquals(4, context.getPendingCount()); + clock.now = ANCHOR + HOUR; + for (int i = 0; i < 3; ++i) { + assertEquals(EXPECTED_2_ARMS[1], context.getTreatment("exp_sb")); + } + assertEquals(5, context.getPendingCount()); + + final Exposure[] exposures = publishExposures(context); + assertEquals(exposure(1, "exp_sb", EXPECTED_2_ARMS[0], ANCHOR, true, true), exposures[3]); + assertEquals(exposure(1, "exp_sb", EXPECTED_2_ARMS[1], ANCHOR + HOUR, true, true), exposures[4]); + } + + // An overridden switchback slot is re-resolved on a grid change like a normal one; the + // override still wins and the holdout still fires exactly once. + @Test + void overrideIsReresolvedWhenTheGridChanges() { + final Experiment holdout = holdout(new double[]{0.0, 1.0}); + + final Experiment original = switchback(3, HOUR); + original.holdoutIds = new int[]{11}; + final ContextData data = dataOf(original); + data.holdouts = new Experiment[]{holdout}; + final Context context = createContext(data); + context.setOverride("exp_sb", 2); + + clock.now = ANCHOR + 10; + assertEquals(2, context.getTreatment("exp_sb")); + assertEquals(2, context.getVariableValue("sb.arm", -1)); + assertEquals(2, context.getPendingCount()); + + // Same index 0 under the new period length; variant 2 now carries a different value. + final Experiment regridded = switchback(3, 2 * HOUR); + regridded.holdoutIds = new int[]{11}; + regridded.variants[2] = new ExperimentVariant("C", "{\"sb.arm\":22}"); + final ContextData refreshed = dataOf(regridded); + refreshed.holdouts = new Experiment[]{holdout}; + when(dataProvider.getContextData()).thenReturn(CompletableFuture.completedFuture(refreshed)); + context.refresh(); + + assertEquals(2, context.getTreatment("exp_sb")); + assertEquals(22, context.getVariableValue("sb.arm", -1)); + assertEquals(3, context.getPendingCount()); + + final Exposure[] exposures = publishExposures(context); + assertEquals(new Exposure(11, "holdout_a", UNIT_TYPE, 1, ANCHOR + 10, true, true, false, false, false, + false), exposures[1]); + for (final int i : new int[]{0, 2}) { + assertEquals(new Exposure(1, "exp_sb", UNIT_TYPE, 2, ANCHOR + 10, false, true, true, false, false, + false), exposures[i]); + } + } + + // Each identity transition is a new epoch: returning to an identical grid re-resolves instead of + // resurrecting the slot from before the transition. + @Test + void switchbackToUnitToIdenticalSwitchbackReresolves() { + final Context context = createContext(dataOf(switchback(2, HOUR))); + clock.now = ANCHOR + 10; + assertEquals(EXPECTED_2_ARMS[0], context.getTreatment("exp_sb")); + assertEquals(1, context.getPendingCount()); + + final Experiment toUnit = switchback(2, HOUR); + toUnit.randomizationType = "unit"; + refreshTo(context, toUnit); + context.getTreatment("exp_sb"); + assertEquals(2, context.getPendingCount()); + + refreshTo(context, switchback(2, HOUR)); + assertEquals(EXPECTED_2_ARMS[0], context.getTreatment("exp_sb")); + assertEquals(EXPECTED_2_ARMS[0], context.getTreatment("exp_sb")); + assertEquals(3, context.getPendingCount()); + } + + @Test + void removedAndRestoredSwitchbackReresolves() { + final Context context = createContext(dataOf(switchback(2, HOUR))); + clock.now = ANCHOR + 10; + context.getTreatment("exp_sb"); + + refreshTo(context); + assertEquals(0, context.getTreatment("exp_sb")); + assertEquals(2, context.getPendingCount()); + + refreshTo(context, switchback(2, HOUR)); + assertEquals(EXPECTED_2_ARMS[0], context.getTreatment("exp_sb")); + assertEquals(3, context.getPendingCount()); + } + + // The pre-anchor bare-name slot shares the lifecycle of the period slots. + @Test + void preAnchorSlotIsDroppedWhenTheGridChanges() { + final Experiment gridA = switchback(2, HOUR); + gridA.switchbackStartAt = ANCHOR + 10 * HOUR; + final Context context = createContext(dataOf(gridA)); + clock.now = ANCHOR + 10; + assertEquals(0, context.getTreatment("exp_sb")); + assertEquals(1, context.getPendingCount()); + + refreshTo(context, switchback(2, HOUR)); + assertEquals(EXPECTED_2_ARMS[0], context.getTreatment("exp_sb")); + assertEquals(2, context.getPendingCount()); + + refreshTo(context, gridA); + assertEquals(0, context.getTreatment("exp_sb")); + assertEquals(3, context.getPendingCount()); + } + + // Epochs are reconciled lazily; an intermediate state that is never looked up must still end the + // epoch. + @Test + void transitionsWithoutAnInterveningLookupStillReresolve() { + final Context context = createContext(dataOf(switchback(2, HOUR))); + clock.now = ANCHOR + 10; + context.getTreatment("exp_sb"); + assertEquals(1, context.getPendingCount()); + + final Experiment toUnit = switchback(2, HOUR); + toUnit.randomizationType = "unit"; + refreshTo(context, toUnit); + refreshTo(context, switchback(2, HOUR)); + assertEquals(EXPECTED_2_ARMS[0], context.getTreatment("exp_sb")); + assertEquals(2, context.getPendingCount()); + + refreshTo(context); + refreshTo(context, switchback(2, HOUR)); + assertEquals(EXPECTED_2_ARMS[0], context.getTreatment("exp_sb")); + assertEquals(3, context.getPendingCount()); + + final Experiment regridded = switchback(2, HOUR); + regridded.switchbackDesignType = "other"; + refreshTo(context, regridded); + refreshTo(context, switchback(2, HOUR)); + assertEquals(EXPECTED_2_ARMS[0], context.getTreatment("exp_sb")); + assertEquals(EXPECTED_2_ARMS[0], context.getTreatment("exp_sb")); + assertEquals(4, context.getPendingCount()); + + // An unchanged identity keeps the epoch. + refreshTo(context, switchback(2, HOUR)); + context.getTreatment("exp_sb"); + assertEquals(4, context.getPendingCount()); + } + + // Audience evaluation runs under contextLock_; this matcher refreshes the context from inside it. + static final class RefreshingAudienceMatcher extends AudienceMatcher { + volatile Context context; + volatile boolean refreshOnNextEvaluation; + + RefreshingAudienceMatcher() { + super(new DefaultAudienceDeserializer()); + } + + @Override + public Result evaluate(String audience, Map attributes) { + if (refreshOnNextEvaluation) { + refreshOnNextEvaluation = false; + context.refresh(); + } + return new Result(true); + } + } + + void stubRefreshData(boolean completeOnAnotherThread, ContextData data) { + if (!completeOnAnotherThread) { + when(dataProvider.getContextData()).thenReturn(CompletableFuture.completedFuture(data)); + return; + } + when(dataProvider.getContextData()).thenAnswer(invocation -> { + final CompletableFuture future = new CompletableFuture(); + // Completes only once refreshAsync has attached its continuation, so setData runs on + // this thread rather than inline on the caller. + final Thread completer = new Thread(() -> { + while (future.getNumberOfDependents() == 0) { + Thread.yield(); + } + future.complete(data); + }); + completer.setDaemon(true); + completer.start(); + return future; + }); + } + + // Runs getTreatment on a daemon worker and fails, rather than hanging the build, if it does not + // return within the bound. + int getTreatmentWithin(Context context, String name, long timeoutMs) throws InterruptedException { + final int[] result = new int[1]; + final Throwable[] failure = new Throwable[1]; + final Thread worker = new Thread(() -> { + try { + result[0] = context.getTreatment(name); + } catch (Throwable t) { + failure[0] = t; + } + }); + worker.setDaemon(true); + worker.start(); + worker.join(timeoutMs); + assertFalse(worker.isAlive(), "getTreatment did not return: refresh from audience evaluation deadlocked"); + if (failure[0] != null) { + throw new AssertionError(failure[0]); + } + return result[0]; + } + + void refreshFromAudienceRevalidationCompletes(boolean completeOnAnotherThread) throws InterruptedException { + final Experiment unit = switchback(2, HOUR); + unit.randomizationType = "unit"; + unit.audience = "{\"filter\":[{\"value\":true}]}"; + final RefreshingAudienceMatcher matcher = new RefreshingAudienceMatcher(); + final Context context = createContext(TestUtils.mapOf(UNIT_TYPE, UID), dataOf(unit), matcher); + matcher.context = context; + stubRefreshData(completeOnAnotherThread, dataOf(unit)); + + final int primed = context.getTreatment("exp_sb"); + context.setAttribute("country", "PT"); + matcher.refreshOnNextEvaluation = true; + + assertEquals(primed, getTreatmentWithin(context, "exp_sb", 10_000)); + verify(dataProvider, times(1)).getContextData(); + assertEquals(1, context.getPendingCount()); + } + + @Test + void refreshFromAudienceRevalidationWithACompletedProviderFutureDoesNotDeadlock() + throws InterruptedException { + refreshFromAudienceRevalidationCompletes(false); + } + + @Test + void refreshFromAudienceRevalidationWithAnAsyncProviderDoesNotDeadlock() throws InterruptedException { + refreshFromAudienceRevalidationCompletes(true); + } + + // First resolution evaluates the audience under the contextLock_ write lock. + @Test + void refreshFromAudienceEvaluationOnFirstResolutionWithAnAsyncProviderDoesNotDeadlock() + throws InterruptedException { + final Experiment unit = switchback(2, HOUR); + unit.randomizationType = "unit"; + unit.audience = "{\"filter\":[{\"value\":true}]}"; + final RefreshingAudienceMatcher matcher = new RefreshingAudienceMatcher(); + final Context context = createContext(TestUtils.mapOf(UNIT_TYPE, UID), dataOf(unit), matcher); + matcher.context = context; + stubRefreshData(true, dataOf(unit)); + matcher.refreshOnNextEvaluation = true; + + getTreatmentWithin(context, "exp_sb", 10_000); + verify(dataProvider, times(1)).getContextData(); + assertEquals(1, context.getPendingCount()); + } + + // Holds both revalidating readers at a barrier, so both have rejected the cached slot under + // contextLock_ read before either can take the write lock. + static final class BarrierAudienceMatcher extends AudienceMatcher { + final AtomicInteger evaluations = new AtomicInteger(); + final CyclicBarrier readers = new CyclicBarrier(2); + + BarrierAudienceMatcher() { + super(new DefaultAudienceDeserializer()); + } + + @Override + public Result evaluate(String audience, Map attributes) { + final int call = evaluations.incrementAndGet(); + if (call == 1) { + return new Result(true); + } + if (call <= 3) { + try { + readers.await(10, TimeUnit.SECONDS); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + return new Result(false); + } + } + + @Test + void concurrentResolutionOfTheSamePeriodExposesOnce() throws InterruptedException { + final Experiment experiment = switchback(2, HOUR); + experiment.audience = "{\"filter\":[{\"value\":true}]}"; + resolvePeriodOneConcurrently(experiment, null, EXPECTED_2_ARMS[1]); + } + + // Attribute-sourced: the cluster attribute changes before both readers re-resolve the slot, and + // a third worker keeps writing it meanwhile. The slot keeps the key read at first resolution + // (Lisboa, variant 1 in period 1); Porto is variant 0 there, so a re-read key is visible. + @Test + void concurrentResolutionWithConcurrentSetAttributeExposesOnce() throws InterruptedException { + final Experiment experiment = switchback(2, HOUR); + experiment.audience = "{\"filter\":[{\"value\":true}]}"; + experiment.switchbackClusterAttribute = "city"; + resolvePeriodOneConcurrently(experiment, "Porto", 1); + } + + void resolvePeriodOneConcurrently(Experiment experiment, String concurrentCity, int expectedVariant) + throws InterruptedException { + final BarrierAudienceMatcher matcher = new BarrierAudienceMatcher(); + final Context context = createContext(TestUtils.mapOf(UNIT_TYPE, UID), dataOf(experiment), matcher); + clock.now = ANCHOR + HOUR + 10; + context.setAttribute("city", "Lisboa"); + + // Caches period 1 without exposing it; the attribute change forces both callers to revalidate. + assertEquals(expectedVariant, context.peekTreatment("exp_sb")); + context.setAttribute("country", "PT"); + if (concurrentCity != null) { + context.setAttribute("city", concurrentCity); + } + + final int[] variants = new int[2]; + final Throwable[] failures = new Throwable[3]; + final Thread[] workers = new Thread[(concurrentCity != null) ? 3 : 2]; + for (int i = 0; i < 2; ++i) { + final int slot = i; + workers[i] = new Thread(() -> { + try { + variants[slot] = context.getTreatment("exp_sb"); + } catch (Throwable t) { + failures[slot] = t; + } + }); + } + if (concurrentCity != null) { + final AtomicInteger writes = new AtomicInteger(); + workers[2] = new Thread(() -> { + try { + // Starts once both readers are in the audience barrier, so no reader blocks + // on a queued writer before the barrier trips. + while ((matcher.evaluations.get() < 3) && (workers[0].isAlive() || workers[1].isAlive())) { + Thread.yield(); + } + while ((workers[0].isAlive() || workers[1].isAlive() || writes.get() < 1000) + && writes.get() < 100_000) { + context.setAttribute("city", concurrentCity); + writes.incrementAndGet(); + } + } catch (Throwable t) { + failures[2] = t; + } + }); + } + for (final Thread worker : workers) { + worker.setDaemon(true); + worker.start(); + } + for (final Thread worker : workers) { + worker.join(10_000); + assertFalse(worker.isAlive()); + } + assertArrayEquals(new Throwable[3], failures); + + assertEquals(expectedVariant, variants[0]); + assertEquals(expectedVariant, variants[1]); + assertEquals(1, context.getPendingCount()); + assertArrayEquals(new Exposure[]{new Exposure(1, "exp_sb", UNIT_TYPE, expectedVariant, ANCHOR + HOUR + 10, + true, true, false, false, false, true)}, publishExposures(context)); + } +} diff --git a/core-api/src/test/java/com/absmartly/sdk/DefaultContextDataDeserializerTest.java b/core-api/src/test/java/com/absmartly/sdk/DefaultContextDataDeserializerTest.java index fb3b2cf..d8afc2e 100644 --- a/core-api/src/test/java/com/absmartly/sdk/DefaultContextDataDeserializerTest.java +++ b/core-api/src/test/java/com/absmartly/sdk/DefaultContextDataDeserializerTest.java @@ -196,6 +196,90 @@ void deserializeHoldouts() { assertEquals(expected, data); } + @Test + void deserializeSwitchback() { + final byte[] bytes = getResourceBytes("switchback_context.json"); + + final ContextDataDeserializer deser = new DefaultContextDataDeserializer(); + final ContextData data = deser.deserialize(bytes, 0, bytes.length); + + final Experiment switchback = new Experiment(); + switchback.id = 1; + switchback.name = "exp_test_switchback"; + switchback.unitType = "store_id"; + switchback.iteration = 1; + switchback.seedHi = 3603515; + switchback.seedLo = 233373850; + switchback.split = new double[]{0.5, 0.5}; + switchback.trafficSeedHi = 449867249; + switchback.trafficSeedLo = 455443629; + switchback.trafficSplit = new double[]{0.0, 1.0}; + switchback.fullOnVariant = 0; + switchback.applications = new ExperimentApplication[]{new ExperimentApplication("website")}; + switchback.variants = new ExperimentVariant[]{ + new ExperimentVariant("A", null), + new ExperimentVariant("B", "{\"banner.border\":1}") + }; + switchback.randomizationType = "switchback"; + switchback.switchbackClusterUnitType = "store_id"; + switchback.switchbackStartAt = 1767225600000L; + switchback.switchbackPeriodMs = 3600000L; + switchback.switchbackCarryoverDurationMs = 900000L; + switchback.switchbackBurnInDurationMs = 600000L; + switchback.switchbackBurnOutDurationMs = 0L; + switchback.switchbackDesignType = "balanced_block"; + switchback.switchbackEstimand = "global_average_treatment_effect"; + switchback.switchbackClusterCount = 50; + switchback.switchbackClusterAttribute = "geo/city"; + + final Experiment unit = new Experiment(); + unit.id = 2; + unit.name = "exp_test_unit"; + unit.unitType = "session_id"; + unit.iteration = 1; + unit.seedHi = 55006150; + unit.seedLo = 47189152; + unit.split = new double[]{0.5, 0.5}; + unit.trafficSeedHi = 705671872; + unit.trafficSeedLo = 212903484; + unit.trafficSplit = new double[]{0.0, 1.0}; + unit.fullOnVariant = 0; + unit.applications = new ExperimentApplication[]{new ExperimentApplication("website")}; + unit.variants = new ExperimentVariant[]{ + new ExperimentVariant("A", null), + new ExperimentVariant("B", null) + }; + unit.randomizationType = "unit"; + + final ContextData expected = new ContextData(); + expected.experiments = new Experiment[]{switchback, unit}; + + assertNotNull(data); + assertEquals(expected, data); + assertEquals(expected.hashCode(), data.hashCode()); + } + + @Test + void deserializeWithoutSwitchbackFieldsLeavesThemNull() { + final byte[] bytes = getResourceBytes("context.json"); + + final ContextData data = new DefaultContextDataDeserializer().deserialize(bytes, 0, bytes.length); + + for (final Experiment experiment : data.experiments) { + assertNull(experiment.randomizationType); + assertNull(experiment.switchbackClusterUnitType); + assertNull(experiment.switchbackStartAt); + assertNull(experiment.switchbackPeriodMs); + assertNull(experiment.switchbackCarryoverDurationMs); + assertNull(experiment.switchbackBurnInDurationMs); + assertNull(experiment.switchbackBurnOutDurationMs); + assertNull(experiment.switchbackDesignType); + assertNull(experiment.switchbackEstimand); + assertNull(experiment.switchbackClusterCount); + assertNull(experiment.switchbackClusterAttribute); + } + } + @Test void testMalformedJsonResponse() { final ContextDataDeserializer deser = new DefaultContextDataDeserializer(); diff --git a/core-api/src/test/java/com/absmartly/sdk/SwitchbackServerParityTest.java b/core-api/src/test/java/com/absmartly/sdk/SwitchbackServerParityTest.java new file mode 100644 index 0000000..3ba04f4 --- /dev/null +++ b/core-api/src/test/java/com/absmartly/sdk/SwitchbackServerParityTest.java @@ -0,0 +1,324 @@ +package com.absmartly.sdk; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ScheduledExecutorService; +import java8.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import com.absmartly.sdk.internal.SwitchbackAssigner; +import com.absmartly.sdk.java.time.Clock; +import com.absmartly.sdk.json.ContextData; +import com.absmartly.sdk.json.Experiment; +import com.absmartly.sdk.json.ExperimentApplication; +import com.absmartly.sdk.json.ExperimentVariant; +import com.absmartly.sdk.json.Exposure; + +// Serves contexts through the public Context API and compares every exposure with the Collector's +// ContextAPI.switchbackVariant: -1 (variant 0, not eligible) for an unservable experiment or a +// missing bucket key, otherwise assignSwitchback(bucketOf(key), periodIndex, seed, armCount). The +// expected bucket is read from bucket-golden-vectors.json, never computed by SDK code, and the +// expected path outcome from attribute-path-cases.json. +class SwitchbackServerParityTest { + static final String UNIT_TYPE = "store_id"; + static final long HOUR = 3_600_000L; + static final long ANCHOR = 1_767_225_600_000L; + static final int[] COUNTS = {10, 50, 100}; + static final int[] ARM_COUNTS = {2, 3, 10}; + static final int PERIODS = 4; + + static final ObjectMapper MAPPER = new ObjectMapper(); + static JsonNode bucketVectors; + static JsonNode pathCases; + // "key\u0000seed\u0000count" -> expectedBucket. + static final Map BUCKETS = new HashMap(); + static final List SEEDS = new ArrayList(); + + @BeforeAll + static void loadFixtures() throws IOException { + bucketVectors = read("switchback/bucket-golden-vectors.json"); + pathCases = read("switchback/attribute-path-cases.json"); + for (final JsonNode c : bucketVectors.get("buckets")) { + final long seed = Long.parseLong(c.get("seed").textValue()); + BUCKETS.put(bucketKey(c.get("key").textValue(), seed, c.get("count").intValue()), + c.get("expectedBucket").intValue()); + if (!SEEDS.contains(seed)) { + SEEDS.add(seed); + } + } + assertEquals(9, SEEDS.size()); + } + + static JsonNode read(String resource) throws IOException { + final InputStream in = SwitchbackServerParityTest.class.getClassLoader().getResourceAsStream(resource); + try { + return MAPPER.readTree(in); + } finally { + in.close(); + } + } + + static String bucketKey(String key, long seed, int count) { + return key + '\u0000' + seed + '\u0000' + count; + } + + // ContextAPI.switchbackVariant after the unservable and missing-key checks: the schedule of the + // key's fixture bucket. SwitchbackAssigner is pinned by switchback/golden-vectors.json. + static int collectorVariant(String key, long seed, int count, long periodIndex, int armCount) { + final Integer bucket = BUCKETS.get(bucketKey(key, seed, count)); + assertNotNull(bucket, "no fixture bucket for " + key + " seed " + seed + " count " + count); + return SwitchbackAssigner.assign(String.valueOf(bucket), periodIndex, seed, armCount); + } + + static final class FixedClock extends Clock { + long now = ANCHOR; + + @Override + public long millis() { + return now; + } + } + + static final class ExposureCapture implements ContextEventLogger { + final List exposures = new ArrayList(); + + @Override + public void handleEvent(Context context, EventType type, Object data) { + if (type == EventType.Exposure) { + exposures.add((Exposure) data); + } + } + } + + static Experiment experiment(long seed, int armCount, Integer count, String attribute) { + final Experiment experiment = new Experiment(); + experiment.id = 1; + experiment.name = "exp_sb"; + experiment.unitType = UNIT_TYPE; + experiment.iteration = 1; + experiment.seedHi = (int) (seed >> 32); + experiment.seedLo = (int) seed; + experiment.split = new double[armCount]; + experiment.variants = new ExperimentVariant[armCount]; + for (int i = 0; i < armCount; ++i) { + experiment.split[i] = 1.0 / armCount; + experiment.variants[i] = new ExperimentVariant(String.valueOf(i), null); + } + experiment.trafficSplit = new double[]{0.0, 1.0}; + experiment.applications = new ExperimentApplication[]{new ExperimentApplication("website")}; + experiment.randomizationType = "switchback"; + experiment.switchbackClusterUnitType = UNIT_TYPE; + experiment.switchbackStartAt = ANCHOR; + experiment.switchbackPeriodMs = HOUR; + experiment.switchbackDesignType = "balanced_block"; + experiment.switchbackClusterCount = count; + experiment.switchbackClusterAttribute = attribute; + return experiment; + } + + // Serves PERIODS periods with getTreatment and returns one variant per period, plus the + // exposures: one per period, or a single one for an unservable grid, whose periods share one + // slot. + static Served serve(Experiment experiment, String uid, Map attributes) { + final FixedClock clock = new FixedClock(); + final ExposureCapture capture = new ExposureCapture(); + final ContextConfig config = ContextConfig.create(); + config.setUnits(TestUtils.mapOf(UNIT_TYPE, uid)); + config.setEventLogger(capture); + final ContextData data = new ContextData(); + data.experiments = new Experiment[]{experiment}; + final ContextEventHandler handler = mock(ContextEventHandler.class); + when(handler.publish(any(), any())).thenReturn(CompletableFuture. completedFuture(null)); + final Context context = Context.create(clock, config, mock(ScheduledExecutorService.class), + CompletableFuture.completedFuture(data), mock(ContextDataProvider.class), handler, null, + new DefaultVariableParser(), new AudienceMatcher(new DefaultAudienceDeserializer())); + if (attributes != null) { + context.setAttributes(attributes); + } + final List variants = new ArrayList(); + for (int p = 0; p < PERIODS; ++p) { + clock.now = ANCHOR + p * HOUR + 1; + variants.add(context.getTreatment("exp_sb")); + } + return new Served(variants, capture.exposures); + } + + static final class Served { + final List variants; + final List exposures; + + Served(List variants, List exposures) { + this.variants = variants; + this.exposures = exposures; + } + } + + static void assertServed(Served served, String key, long seed, int count, int armCount, String message) { + assertTrue(!served.exposures.isEmpty(), message); + for (int p = 0; p < PERIODS; ++p) { + final int expected = (key == null) ? 0 : collectorVariant(key, seed, count, p, armCount); + assertEquals(expected, served.variants.get(p).intValue(), message); + } + for (int i = 0; i < served.exposures.size(); ++i) { + final Exposure exposure = served.exposures.get(i); + if (key == null) { + assertEquals(0, exposure.variant, message); + assertEquals(false, exposure.eligible, message); + } else { + assertEquals(PERIODS, served.exposures.size(), message); + assertEquals(collectorVariant(key, seed, count, i, armCount), exposure.variant, message); + assertEquals(true, exposure.eligible, message); + } + assertEquals(true, exposure.assigned, message); + } + } + + // Unit source: the bucket key is UnitHasher.hash(uid). The SDK keeps unit uids as strings and + // rejects a blank uid in setUnit, so numeric uids are served in base 10 and "" is skipped. + @Test + void unitSourceMatchesTheCollectorForEveryUnitKeySeedAndCount() { + int served = 0; + for (final JsonNode c : bucketVectors.get("unitKeys")) { + final JsonNode uidNode = c.get("unitUid"); + final String uid = uidNode.isIntegralNumber() ? Long.toString(uidNode.longValue()) : uidNode.textValue(); + if (uid.trim().isEmpty()) { + continue; + } + for (final long seed : SEEDS) { + for (final int count : COUNTS) { + final int armCount = ARM_COUNTS[served % ARM_COUNTS.length]; + assertServed(serve(experiment(seed, armCount, count, null), uid, null), + c.get("key").textValue(), seed, count, armCount, c + " seed " + seed + " count " + count); + ++served; + } + } + } + assertEquals(11 * 9 * 3, served); + } + + // Attribute source at a top-level, a nested and an array path: the key is + // normalizeAttribute(value), or none for "", null, booleans and floats. + @Test + void attributeSourceMatchesTheCollectorForEveryAttributeValueSeedAndCount() { + int served = 0; + int ineligible = 0; + for (final JsonNode c : bucketVectors.get("attributeKeys")) { + final Object value = MAPPER.convertValue(c.get("value"), Object.class); + final String key = c.get("key").isNull() ? null : c.get("key").textValue(); + final Map flat = TestUtils.mapOf("cluster", value); + final Map nested = TestUtils.mapOf("geo", TestUtils.mapOf("city", value)); + final Map array = TestUtils.mapOf("items", + TestUtils.listOf(TestUtils.mapOf("city", "other"), TestUtils.mapOf("city", value))); + + final String[] paths = {"cluster", "geo/city", "items/1/city"}; + final List> attributes = TestUtils.listOf(flat, nested, array); + for (final long seed : SEEDS) { + for (final int count : COUNTS) { + final int armCount = ARM_COUNTS[served % ARM_COUNTS.length]; + final int shape = served % paths.length; + assertServed(serve(experiment(seed, armCount, count, paths[shape]), "u1", attributes.get(shape)), + key, seed, count, armCount, c + " path " + paths[shape] + " seed " + seed); + ++served; + if (key == null) { + ++ineligible; + } + } + } + } + assertEquals(21 * 9 * 3, served); + assertEquals(6 * 9 * 3, ineligible); + } + + // Each path case is served over the fixture's attributes. An unsupported path is unservable and + // a path resolving to null is ineligible. Any other path serves the schedule of its value's + // bucket. The case values are not bucket-fixture keys, so their buckets for counts 10, 50 and + // 100 under the seeds used below were computed with Python hashlib MD5 over the platform + // formula, which reproduces all 702 bucket-fixture vectors. + static final Map PATH_CASE_BUCKETS = TestUtils.mapOf("items/0/city", new int[]{8, 10, 58}, + "geo/+1", new int[]{7, 12, 77}, "geo/city", new int[]{2, 5, 82}); + + @Test + void attributePathCasesMatchTheCollector() { + final Map attributes = MAPPER.convertValue(pathCases.get("attributes"), + new TypeReference>() {}); + int eligibleCases = 0; + for (final JsonNode c : pathCases.get("cases")) { + final String path = c.get("path").textValue(); + final Object value = MAPPER.convertValue(c.get("value"), Object.class); + for (int n = 0; n < COUNTS.length; ++n) { + final int count = COUNTS[n]; + final long seed = SEEDS.get(count % SEEDS.size()); + final Served served = serve(experiment(seed, 3, count, path), "u1", attributes); + if (!c.get("supported").booleanValue() || (value == null)) { + assertServed(served, null, seed, count, 3, c.toString()); + continue; + } + final int bucket = PATH_CASE_BUCKETS.get(path)[n]; + assertEquals(PERIODS, served.exposures.size(), c.toString()); + for (int p = 0; p < PERIODS; ++p) { + assertEquals(true, served.exposures.get(p).eligible, c.toString()); + assertEquals(SwitchbackAssigner.assign(String.valueOf(bucket), p, seed, 3), + served.variants.get(p).intValue(), c + " count " + count + " period " + p); + } + ++eligibleCases; + } + } + assertEquals(3 * COUNTS.length, eligibleCases); + } + + // The Collector's CLUSTER_COUNT unservable rule: any count outside {10, 50, 100}, or none. + @Test + void disallowedCountsAreUnservable() { + final String key = bucketVectors.get("unitKeys").get(0).get("key").textValue(); + final String uid = bucketVectors.get("unitKeys").get(0).get("unitUid").textValue(); + for (final Integer count : new Integer[]{null, 0, 1, 20, 99, 1000}) { + assertServed(serve(experiment(0L, 2, count, null), uid, null), null, 0L, 10, 2, String.valueOf(count)); + } + assertServed(serve(experiment(0L, 2, 10, null), uid, null), key, 0L, 10, 2, "control"); + } + + // The Collector resolves attributes by trimmed name, latest setAt winning. + @Test + void repeatedAttributeNamesResolveToTheLatest() { + final JsonNode keys = bucketVectors.get("attributeKeys"); + final String first = keys.get(0).get("key").textValue(); + final String second = keys.get(1).get("key").textValue(); + final Map attributes = TestUtils.mapOf(" cluster ", first); + assertServed(serve(experiment(0L, 3, 50, "cluster"), "u1", attributes), first, 0L, 50, 3, "trimmed name"); + + final FixedClock clock = new FixedClock(); + final ExposureCapture capture = new ExposureCapture(); + final ContextConfig config = ContextConfig.create(); + config.setUnits(TestUtils.mapOf(UNIT_TYPE, "u1")); + config.setEventLogger(capture); + final ContextData data = new ContextData(); + data.experiments = new Experiment[]{experiment(0L, 3, 50, "cluster")}; + final Context context = Context.create(clock, config, mock(ScheduledExecutorService.class), + CompletableFuture.completedFuture(data), mock(ContextDataProvider.class), + mock(ContextEventHandler.class), null, new DefaultVariableParser(), + new AudienceMatcher(new DefaultAudienceDeserializer())); + context.setAttribute("cluster", first); + context.setAttribute("cluster", second); + clock.now = ANCHOR + 1; + context.getTreatment("exp_sb"); + assertEquals(collectorVariant(second, 0L, 50, 0, 3), capture.exposures.get(0).variant); + assertTrue(capture.exposures.get(0).eligible); + } +} diff --git a/core-api/src/test/java/com/absmartly/sdk/internal/SwitchbackAssignerGoldenVectorsTest.java b/core-api/src/test/java/com/absmartly/sdk/internal/SwitchbackAssignerGoldenVectorsTest.java new file mode 100644 index 0000000..10dad96 --- /dev/null +++ b/core-api/src/test/java/com/absmartly/sdk/internal/SwitchbackAssignerGoldenVectorsTest.java @@ -0,0 +1,131 @@ +package com.absmartly.sdk.internal; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.io.InputStream; +import java.util.HashSet; +import java.util.Set; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +// Fixture copied byte-for-byte from absmartly/abs +// lib/analytics/src/test/resources/switchback/golden-vectors.json (git blob b314abeb9e97). +class SwitchbackAssignerGoldenVectorsTest { + private static JsonNode loadVectors() throws IOException { + final InputStream in = SwitchbackAssignerGoldenVectorsTest.class.getClassLoader() + .getResourceAsStream("switchback/golden-vectors.json"); + try { + return new ObjectMapper().readTree(in); + } finally { + in.close(); + } + } + + // The platform hashes a numeric cluster unit as Long.toString(n); strings pass through. + private static String clusterUnit(JsonNode node) { + if (node.isIntegralNumber()) { + return Long.toString(node.longValue()); + } + assertTrue(node.isTextual(), "clusterUnit must be a string or integer: " + node); + return node.textValue(); + } + + // 64-bit fields are decimal strings; a numeric node would mean the fixture regressed to a + // lossy encoding. + private static long exactLong(JsonNode node) { + assertTrue(node.isTextual(), "64-bit field must be a decimal string: " + node); + return Long.parseLong(node.textValue()); + } + + @Test + void assignSwitchbackMatchesEveryGoldenVector() throws IOException { + final JsonNode cases = loadVectors().get("assignSwitchback"); + assertEquals(882, cases.size()); + + final Set armCounts = new HashSet(); + int boundaryCases = 0; + for (final JsonNode c : cases) { + final String unit = clusterUnit(c.get("clusterUnit")); + final long periodIndex = exactLong(c.get("periodIndex")); + final long seed = exactLong(c.get("seed")); + final int armCount = c.get("armCount").intValue(); + armCounts.add(armCount); + if (periodIndex == Long.MIN_VALUE || periodIndex == Long.MAX_VALUE || seed == Long.MIN_VALUE + || seed == Long.MAX_VALUE) { + ++boundaryCases; + } + + assertEquals(c.get("expectedVariant").intValue(), + SwitchbackAssigner.assign(unit, periodIndex, seed, armCount), + c.toString()); + } + + assertTrue(armCounts.contains(2) && armCounts.contains(3) && armCounts.contains(10), armCounts.toString()); + assertTrue(boundaryCases > 0); + } + + @Test + void fisherYatesPermutationMatchesEveryGoldenVector() throws IOException { + final JsonNode cases = loadVectors().get("fisherYatesPermutation"); + assertEquals(504, cases.size()); + + for (final JsonNode c : cases) { + final String unit = clusterUnit(c.get("clusterUnit")); + final long cycleIndex = exactLong(c.get("cycleIndex")); + final long seed = exactLong(c.get("seed")); + final int armCount = c.get("armCount").intValue(); + + final JsonNode expectedNode = c.get("expectedPermutation"); + final int[] expected = new int[expectedNode.size()]; + for (int i = 0; i < expected.length; ++i) { + expected[i] = expectedNode.get(i).intValue(); + } + + final int[] actual = SwitchbackAssigner.fisherYatesPermutation(armCount, + SwitchbackAssigner.baseHash(unit, cycleIndex, seed)); + assertArrayEquals(expected, actual, c.toString()); + } + } + + @Test + void everyCycleCoversEveryArmExactlyOnce() { + final int[] armCounts = {2, 3, 10}; + for (final int armCount : armCounts) { + for (long cycle = -3; cycle < 20; ++cycle) { + final boolean[] seen = new boolean[armCount]; + for (int slot = 0; slot < armCount; ++slot) { + final int variant = SwitchbackAssigner.assign("store_42", cycle * armCount + slot, + 0x1234_5678_9abcL, + armCount); + assertTrue(!seen[variant], "arm repeated within a cycle"); + seen[variant] = true; + } + } + } + } + + @Test + void rejectsFewerThanTwoArms() { + assertThrows(IllegalArgumentException.class, () -> SwitchbackAssigner.assign("u", 0, 0, 1)); + assertThrows(IllegalArgumentException.class, () -> SwitchbackAssigner.fisherYatesPermutation(0, 0)); + } + + @Test + void floorDiv() { + assertEquals(0, SwitchbackAssigner.floorDiv(0, 3)); + assertEquals(0, SwitchbackAssigner.floorDiv(2, 3)); + assertEquals(1, SwitchbackAssigner.floorDiv(3, 3)); + assertEquals(-1, SwitchbackAssigner.floorDiv(-1, 3)); + assertEquals(-1, SwitchbackAssigner.floorDiv(-3, 3)); + assertEquals(-2, SwitchbackAssigner.floorDiv(-4, 3)); + assertEquals(Math.floorDiv(Long.MIN_VALUE, 3L), SwitchbackAssigner.floorDiv(Long.MIN_VALUE, 3)); + assertEquals(Math.floorDiv(Long.MAX_VALUE, 10L), SwitchbackAssigner.floorDiv(Long.MAX_VALUE, 10)); + } +} diff --git a/core-api/src/test/java/com/absmartly/sdk/internal/SwitchbackAttributePathTest.java b/core-api/src/test/java/com/absmartly/sdk/internal/SwitchbackAttributePathTest.java new file mode 100644 index 0000000..49bf6db --- /dev/null +++ b/core-api/src/test/java/com/absmartly/sdk/internal/SwitchbackAttributePathTest.java @@ -0,0 +1,133 @@ +package com.absmartly.sdk.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import com.absmartly.sdk.TestUtils; + +// Fixture copied byte-for-byte from absmartly/abs +// lib/utils/src/test/resources/switchback/attribute-path-cases.json (git blob 7b1d3fed5985). +class SwitchbackAttributePathTest { + static JsonNode loadCases() throws IOException { + final InputStream in = SwitchbackAttributePathTest.class.getClassLoader() + .getResourceAsStream("switchback/attribute-path-cases.json"); + try { + return new ObjectMapper().readTree(in); + } finally { + in.close(); + } + } + + @Test + void matchesEveryPathCase() throws IOException { + final ObjectMapper mapper = new ObjectMapper(); + final JsonNode fixture = loadCases(); + final Map attributes = mapper.convertValue(fixture.get("attributes"), + new TypeReference>() {}); + final JsonNode cases = fixture.get("cases"); + assertEquals(7, cases.size()); + + for (final JsonNode c : cases) { + final SwitchbackAttributePath path = SwitchbackAttributePath.parseSupported(c.get("path").textValue()); + if (!c.get("supported").booleanValue()) { + assertNull(path, c.toString()); + continue; + } + assertNotNull(path, c.toString()); + assertEquals(mapper.convertValue(c.get("value"), Object.class), path.resolve(attributes), c.toString()); + } + } + + @Test + void quotedSegmentsAreLiteralKeys() { + final Map attributes = TestUtils.mapOf("geo", TestUtils.mapOf("0", "zero", "a/b", "slash")); + assertEquals("zero", SwitchbackAttributePath.parseSupported("geo/'0'").resolve(attributes)); + assertEquals("slash", SwitchbackAttributePath.parseSupported("geo/\"a/b\"").resolve(attributes)); + // An unterminated quoted tail is dropped, so the path resolves to the parent object. + assertEquals(attributes.get("geo"), SwitchbackAttributePath.parseSupported("geo/'0").resolve(attributes)); + } + + @Test + void separatorsAreCollapsed() { + final Map attributes = TestUtils.mapOf("geo", TestUtils.mapOf("city", "Porto")); + assertEquals("Porto", SwitchbackAttributePath.parseSupported("/geo//city/").resolve(attributes)); + assertEquals(attributes.get("geo"), SwitchbackAttributePath.parseSupported("geo").resolve(attributes)); + } + + @Test + void rejectsUnsupportedPaths() { + for (final String path : new String[]{null, "", "/", "//", "0/city", "items/0", "items/0/1/city", + "items/-1/city", "'"}) { + assertNull(SwitchbackAttributePath.parseSupported(path), String.valueOf(path)); + } + } + + @Test + void indexSegmentsReadOnlyListsAndKeySegmentsReadOnlyMaps() { + final Map attributes = TestUtils.mapOf("items", + TestUtils.listOf(TestUtils.mapOf("city", "Lisbon")), "geo", + TestUtils.mapOf("0", TestUtils.mapOf("city", "zero")), + "name", "scalar"); + assertNull(SwitchbackAttributePath.parseSupported("geo/0/city").resolve(attributes)); + assertNull(SwitchbackAttributePath.parseSupported("items/1/city").resolve(attributes)); + assertNull(SwitchbackAttributePath.parseSupported("name/x").resolve(attributes)); + assertNull(SwitchbackAttributePath.parseSupported("missing/x").resolve(attributes)); + assertEquals("Lisbon", SwitchbackAttributePath.parseSupported("items/-0/city").resolve(attributes)); + } + + @Test + void cyclicOrTooDeepValuesHaveNoKey() { + final SwitchbackAttributePath path = SwitchbackAttributePath.parseSupported("geo/city"); + + final Map self = new HashMap(); + self.put("city", "Porto"); + self.put("self", self); + assertNull(path.resolveAttribute(self)); + + final Map outer = new HashMap(); + final List list = new ArrayList(); + list.add(new Object[]{outer}); + outer.put("city", "Porto"); + outer.put("items", list); + assertNull(path.resolveAttribute(outer)); + + // 65 nested maps: one past the depth bound. + Map deep = TestUtils.mapOf("leaf", "x"); + for (int i = 0; i < 64; ++i) { + deep = TestUtils.mapOf("city", "Porto", "next", deep); + } + assertNull(path.resolveAttribute(deep)); + assertEquals("Porto", path.resolveAttribute(deep.get("next"))); + + // A value reached twice through siblings is not a cycle. + final Map shared = TestUtils.mapOf("a", "b"); + assertEquals("Porto", path.resolveAttribute(TestUtils.mapOf("city", "Porto", "x", shared, "y", shared))); + } + + @Test + void tryParseIntFollowsGuava() { + assertEquals(Integer.valueOf(0), SwitchbackAttributePath.tryParseInt("0")); + assertEquals(Integer.valueOf(0), SwitchbackAttributePath.tryParseInt("-0")); + assertEquals(Integer.valueOf(7), SwitchbackAttributePath.tryParseInt("007")); + assertEquals(Integer.valueOf(Integer.MAX_VALUE), SwitchbackAttributePath.tryParseInt("2147483647")); + assertEquals(Integer.valueOf(Integer.MIN_VALUE), SwitchbackAttributePath.tryParseInt("-2147483648")); + for (final String s : new String[]{"", "-", "+1", "1a", " 1", "2147483648", "-2147483649", + "99999999999999999999", "\u0660", "1.0"}) { + assertNull(SwitchbackAttributePath.tryParseInt(s), s); + } + } +} diff --git a/core-api/src/test/java/com/absmartly/sdk/internal/SwitchbackBucketsTest.java b/core-api/src/test/java/com/absmartly/sdk/internal/SwitchbackBucketsTest.java new file mode 100644 index 0000000..16d5e6d --- /dev/null +++ b/core-api/src/test/java/com/absmartly/sdk/internal/SwitchbackBucketsTest.java @@ -0,0 +1,173 @@ +package com.absmartly.sdk.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import com.absmartly.sdk.internal.hashing.Hashing; +import com.absmartly.sdk.java.nio.charset.StandardCharsets; + +// Fixture copied byte-for-byte from absmartly/abs +// lib/utils/src/test/resources/switchback/bucket-golden-vectors.json (git blob c2da44286878). +class SwitchbackBucketsTest { + static JsonNode loadVectors() throws IOException { + final InputStream in = SwitchbackBucketsTest.class.getClassLoader() + .getResourceAsStream("switchback/bucket-golden-vectors.json"); + try { + return new ObjectMapper().readTree(in); + } finally { + in.close(); + } + } + + static int bucketOf(String key, long seed, int count) { + return SwitchbackBuckets.bucketOf((int) (seed >> 32), (int) seed, key, count); + } + + @Test + void bucketOfMatchesEveryGoldenVector() throws IOException { + final JsonNode cases = loadVectors().get("buckets"); + assertEquals(702, cases.size()); + + final Set counts = new HashSet(); + final Set seeds = new HashSet(); + for (final JsonNode c : cases) { + assertTrue(c.get("seed").isTextual(), "64-bit seed must be a decimal string: " + c); + final long seed = Long.parseLong(c.get("seed").textValue()); + final int count = c.get("count").intValue(); + counts.add(count); + seeds.add(seed); + + assertEquals(c.get("expectedBucket").intValue(), bucketOf(c.get("key").textValue(), seed, count), + c.toString()); + } + + assertEquals(new HashSet(Arrays.asList(10, 50, 100)), counts); + assertTrue(seeds.contains(Long.MIN_VALUE) && seeds.contains(Long.MAX_VALUE) && seeds.contains(-1L)); + } + + // Computed with Python hashlib, independently of the generator; the first h is above 2^31. + @Test + void matchesVectorsComputedIndependentlyFromMd5() { + assertEquals(6, bucketOf("1tdwU5K8evYzMovqjExpBA", 0L, 10)); + assertEquals(19, bucketOf("\u6771\u4eac", -123456789012345L, 50)); + assertEquals(43, bucketOf("a\u001fb", Long.MIN_VALUE, 100)); + } + + @Test + void unitKeyIsTheSdkUnitHash() throws IOException { + final JsonNode cases = loadVectors().get("unitKeys"); + assertEquals(12, cases.size()); + for (final JsonNode c : cases) { + final JsonNode uid = c.get("unitUid"); + final String normalized = uid.isIntegralNumber() ? Long.toString(uid.longValue()) : uid.textValue(); + assertEquals(c.get("key").textValue(), + new String(Hashing.hashUnit(normalized), StandardCharsets.US_ASCII), c.toString()); + } + } + + @Test + void normalizeAttributeMatchesEveryGoldenVector() throws IOException { + final ObjectMapper mapper = new ObjectMapper(); + final JsonNode cases = loadVectors().get("attributeKeys"); + assertEquals(21, cases.size()); + int ineligible = 0; + for (final JsonNode c : cases) { + // Decoded the way the SDK's attribute values arrive from JSON: Integer, Long, + // BigInteger, Double, Boolean, String or null. + final Object value = mapper.convertValue(c.get("value"), Object.class); + final JsonNode key = c.get("key"); + if (key.isNull()) { + ++ineligible; + assertNull(SwitchbackBuckets.normalizeAttribute(value), c.toString()); + } else { + assertEquals(key.textValue(), SwitchbackBuckets.normalizeAttribute(value), c.toString()); + } + } + assertEquals(6, ineligible); + } + + @Test + void normalizeAttributeAcceptsEveryIntegerTypeAndRejectsOthers() { + assertEquals("-3", SwitchbackBuckets.normalizeAttribute((byte) -3)); + assertEquals("300", SwitchbackBuckets.normalizeAttribute((short) 300)); + assertEquals("7", SwitchbackBuckets.normalizeAttribute(7)); + assertEquals("-9223372036854775808", SwitchbackBuckets.normalizeAttribute(Long.MIN_VALUE)); + assertEquals("123456789012345678901234567890", + SwitchbackBuckets.normalizeAttribute(new BigInteger("123456789012345678901234567890"))); + assertNull(SwitchbackBuckets.normalizeAttribute(2.0f)); + assertNull(SwitchbackBuckets.normalizeAttribute(new BigDecimal("2"))); + assertNull(SwitchbackBuckets.normalizeAttribute(Collections.emptyMap())); + assertNull(SwitchbackBuckets.normalizeAttribute(Collections.emptyList())); + assertNull(SwitchbackBuckets.normalizeAttribute('c')); + } + + // Interleaves the three hashlib vectors across threads; a digest shared between threads mixes + // their inputs and yields wrong buckets. + @Test + void bucketOfIsThreadSafe() throws InterruptedException { + final String[] keys = {"1tdwU5K8evYzMovqjExpBA", "\u6771\u4eac", "a\u001fb"}; + final long[] seeds = {0L, -123456789012345L, Long.MIN_VALUE}; + final int[] counts = {10, 50, 100}; + final int[] buckets = {6, 19, 43}; + final int threads = 8; + final int iterations = 200000; + final CountDownLatch start = new CountDownLatch(1); + final AtomicInteger wrong = new AtomicInteger(); + final Thread[] workers = new Thread[threads]; + for (int t = 0; t < threads; ++t) { + final int offset = t; + workers[t] = new Thread(() -> { + try { + start.await(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + for (int i = 0; i < iterations; ++i) { + final int k = (i + offset) % keys.length; + if (bucketOf(keys[k], seeds[k], counts[k]) != buckets[k]) { + wrong.incrementAndGet(); + } + } + }); + workers[t].start(); + } + start.countDown(); + for (final Thread worker : workers) { + worker.join(60000); + assertFalse(worker.isAlive()); + } + assertEquals(0, wrong.get()); + } + + @Test + void onlyTenFiftyAndHundredAreAllowed() { + for (final int count : new int[]{10, 50, 100}) { + assertTrue(SwitchbackBuckets.isAllowedCount(count)); + } + for (final Integer count : new Integer[]{null, 0, 1, 2, 20, 99, 101, 1000, -10}) { + assertFalse(SwitchbackBuckets.isAllowedCount(count)); + } + assertThrows(IllegalArgumentException.class, () -> SwitchbackBuckets.bucketOf(0, 0, "k", 1000)); + assertThrows(IllegalArgumentException.class, () -> SwitchbackBuckets.bucketOf(0, 0, "k", 0)); + } +} diff --git a/core-api/src/test/java/com/absmartly/sdk/json/ExperimentSwitchbackFieldsTest.java b/core-api/src/test/java/com/absmartly/sdk/json/ExperimentSwitchbackFieldsTest.java new file mode 100644 index 0000000..64dcc2c --- /dev/null +++ b/core-api/src/test/java/com/absmartly/sdk/json/ExperimentSwitchbackFieldsTest.java @@ -0,0 +1,130 @@ +package com.absmartly.sdk.json; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +class ExperimentSwitchbackFieldsTest { + private static Experiment switchback() { + final Experiment experiment = new Experiment(); + experiment.id = 1; + experiment.name = "exp"; + experiment.unitType = "store_id"; + experiment.randomizationType = "switchback"; + experiment.switchbackClusterUnitType = "store_id"; + experiment.switchbackStartAt = 1767225600000L; + experiment.switchbackPeriodMs = 3600000L; + experiment.switchbackCarryoverDurationMs = 900000L; + experiment.switchbackBurnInDurationMs = 600000L; + experiment.switchbackBurnOutDurationMs = 0L; + experiment.switchbackDesignType = "balanced_block"; + experiment.switchbackEstimand = "global_average_treatment_effect"; + experiment.switchbackClusterCount = 50; + experiment.switchbackClusterAttribute = "geo/city"; + return experiment; + } + + private interface Mutation { + void apply(Experiment e); + } + + @Test + void equalsAndHashCodeCoverEverySwitchbackField() { + assertEquals(switchback(), switchback()); + assertEquals(switchback().hashCode(), switchback().hashCode()); + + final Mutation[] mutations = new Mutation[]{ + new Mutation() { + public void apply(Experiment e) { + e.randomizationType = "unit"; + } + }, + new Mutation() { + public void apply(Experiment e) { + e.switchbackClusterUnitType = "user_id"; + } + }, + new Mutation() { + public void apply(Experiment e) { + e.switchbackStartAt = 1767229200000L; + } + }, + new Mutation() { + public void apply(Experiment e) { + e.switchbackPeriodMs = 86400000L; + } + }, + new Mutation() { + public void apply(Experiment e) { + e.switchbackCarryoverDurationMs = null; + } + }, + new Mutation() { + public void apply(Experiment e) { + e.switchbackBurnInDurationMs = 1L; + } + }, + new Mutation() { + public void apply(Experiment e) { + e.switchbackBurnOutDurationMs = 1L; + } + }, + new Mutation() { + public void apply(Experiment e) { + e.switchbackDesignType = "other"; + } + }, + new Mutation() { + public void apply(Experiment e) { + e.switchbackEstimand = "other"; + } + }, + new Mutation() { + public void apply(Experiment e) { + e.switchbackClusterCount = 100; + } + }, + new Mutation() { + public void apply(Experiment e) { + e.switchbackClusterCount = null; + } + }, + new Mutation() { + public void apply(Experiment e) { + e.switchbackClusterAttribute = null; + } + }, + new Mutation() { + public void apply(Experiment e) { + e.switchbackClusterAttribute = "geo/country"; + } + }, + }; + + for (final Mutation mutation : mutations) { + final Experiment changed = switchback(); + mutation.apply(changed); + assertNotEquals(switchback(), changed); + assertNotEquals(changed, switchback()); + assertNotEquals(switchback().hashCode(), changed.hashCode()); + } + } + + @Test + void toStringIncludesSwitchbackFields() { + final String s = switchback().toString(); + assertTrue(s.contains("randomizationType='switchback'")); + assertTrue(s.contains("switchbackClusterUnitType='store_id'")); + assertTrue(s.contains("switchbackStartAt=1767225600000")); + assertTrue(s.contains("switchbackPeriodMs=3600000")); + assertTrue(s.contains("switchbackCarryoverDurationMs=900000")); + assertTrue(s.contains("switchbackBurnInDurationMs=600000")); + assertTrue(s.contains("switchbackBurnOutDurationMs=0")); + assertTrue(s.contains("switchbackDesignType='balanced_block'")); + assertTrue(s.contains("switchbackEstimand='global_average_treatment_effect'")); + assertTrue(s.contains("switchbackClusterCount=50")); + assertTrue(s.contains("switchbackClusterAttribute='geo/city'")); + } +} diff --git a/core-api/src/test/resources/switchback/attribute-path-cases.json b/core-api/src/test/resources/switchback/attribute-path-cases.json new file mode 100644 index 0000000..7b1d3fe --- /dev/null +++ b/core-api/src/test/resources/switchback/attribute-path-cases.json @@ -0,0 +1,16 @@ +{ + "description": "Cluster attribute paths read by both the Collector (SwitchbackAttributePath) and analysis (ClickHouseSwitchbackExpr). An unquoted segment is an array index exactly when Guava Ints.tryParse parses it (no '+', ASCII digits only). supported: accepted by isSupported and by requireSupportedAttributePath; a negative index is unsupported because ClickHouse throws ZERO_ARRAY_OR_TUPLE_INDEX on the [0] subscript pathToArgs renders for it. value: what the path resolves to in attributes; null means missing (no cluster key).", + "attributes": { + "items": [{ "city": "Lisbon" }, { "city": "Faro" }], + "geo": { "city": "Porto", "-1": "minus-one-key", "+1": "plus-one-key" } + }, + "cases": [ + { "path": "items/0/city", "supported": true, "value": "Lisbon" }, + { "path": "items/+0/city", "supported": true, "value": null }, + { "path": "items/-1/city", "supported": false, "value": null }, + { "path": "geo/-1", "supported": false, "value": null }, + { "path": "geo/+1", "supported": true, "value": "plus-one-key" }, + { "path": "items/\u0660/city", "supported": true, "value": null }, + { "path": "geo/city", "supported": true, "value": "Porto" } + ] +} diff --git a/core-api/src/test/resources/switchback/bucket-golden-vectors.json b/core-api/src/test/resources/switchback/bucket-golden-vectors.json new file mode 100644 index 0000000..c2da442 --- /dev/null +++ b/core-api/src/test/resources/switchback/bucket-golden-vectors.json @@ -0,0 +1,4352 @@ +{ + "unitKeys" : [ + { + "unitUid" : "user-1", + "key" : "1tdwU5K8evYzMovqjExpBA" + }, + { + "unitUid" : "123", + "key" : "ICy5YqxZB1uWSwcVLSNLcA" + }, + { + "unitUid" : 123, + "key" : "ICy5YqxZB1uWSwcVLSNLcA" + }, + { + "unitUid" : 0, + "key" : "z80ghJXVZe9m59_5-Ydk2g" + }, + { + "unitUid" : -42, + "key" : "jfy4n9hiDj5_tqA6U_MH3A" + }, + { + "unitUid" : 9007199254740991, + "key" : "i_qa0JD7v4flGPHOJKk-7g" + }, + { + "unitUid" : "", + "key" : "1B2M2Y8AsgTpgAmY7PhCfg" + }, + { + "unitUid" : "São Paulo", + "key" : "6nbArp3YF-tEj9Gz22JTuw" + }, + { + "unitUid" : "東京", + "key" : "cHuhfH742e8Is57zFK30Mg" + }, + { + "unitUid" : "😀", + "key" : "KgLqw51xanDs83V5GFkntg" + }, + { + "unitUid" : "a\u001Fb", + "key" : "3eFBzFDWEYpd2q4aAbdPmQ" + }, + { + "unitUid" : "x'y\\z", + "key" : "WOAvK4WUCN3v173kQ-NR-g" + } + ], + "attributeKeys" : [ + { + "value" : "Lisboa", + "key" : "Lisboa" + }, + { + "value" : "São Paulo", + "key" : "São Paulo" + }, + { + "value" : "東京", + "key" : "東京" + }, + { + "value" : "😀", + "key" : "😀" + }, + { + "value" : "42", + "key" : "42" + }, + { + "value" : 42, + "key" : "42" + }, + { + "value" : -7, + "key" : "-7" + }, + { + "value" : 0, + "key" : "0" + }, + { + "value" : 9007199254740991, + "key" : "9007199254740991" + }, + { + "value" : -9007199254740991, + "key" : "-9007199254740991" + }, + { + "value" : 18446744073709551615, + "key" : "18446744073709551615" + }, + { + "value" : "a b", + "key" : "a b" + }, + { + "value" : " lead", + "key" : " lead" + }, + { + "value" : "x'y\\z", + "key" : "x'y\\z" + }, + { + "value" : "a\u001Fb", + "key" : "a\u001Fb" + }, + { + "value" : "", + "key" : null + }, + { + "value" : null, + "key" : null + }, + { + "value" : true, + "key" : null + }, + { + "value" : false, + "key" : null + }, + { + "value" : 1.5, + "key" : null + }, + { + "value" : 2.0, + "key" : null + } + ], + "buckets" : [ + { + "key" : "1tdwU5K8evYzMovqjExpBA", + "seed" : "0", + "count" : 10, + "expectedBucket" : 6 + }, + { + "key" : "1tdwU5K8evYzMovqjExpBA", + "seed" : "0", + "count" : 50, + "expectedBucket" : 6 + }, + { + "key" : "1tdwU5K8evYzMovqjExpBA", + "seed" : "0", + "count" : 100, + "expectedBucket" : 6 + }, + { + "key" : "1tdwU5K8evYzMovqjExpBA", + "seed" : "1", + "count" : 10, + "expectedBucket" : 5 + }, + { + "key" : "1tdwU5K8evYzMovqjExpBA", + "seed" : "1", + "count" : 50, + "expectedBucket" : 5 + }, + { + "key" : "1tdwU5K8evYzMovqjExpBA", + "seed" : "1", + "count" : 100, + "expectedBucket" : 5 + }, + { + "key" : "1tdwU5K8evYzMovqjExpBA", + "seed" : "-1", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : "1tdwU5K8evYzMovqjExpBA", + "seed" : "-1", + "count" : 50, + "expectedBucket" : 40 + }, + { + "key" : "1tdwU5K8evYzMovqjExpBA", + "seed" : "-1", + "count" : 100, + "expectedBucket" : 40 + }, + { + "key" : "1tdwU5K8evYzMovqjExpBA", + "seed" : "42", + "count" : 10, + "expectedBucket" : 9 + }, + { + "key" : "1tdwU5K8evYzMovqjExpBA", + "seed" : "42", + "count" : 50, + "expectedBucket" : 49 + }, + { + "key" : "1tdwU5K8evYzMovqjExpBA", + "seed" : "42", + "count" : 100, + "expectedBucket" : 99 + }, + { + "key" : "1tdwU5K8evYzMovqjExpBA", + "seed" : "-123456789012345", + "count" : 10, + "expectedBucket" : 6 + }, + { + "key" : "1tdwU5K8evYzMovqjExpBA", + "seed" : "-123456789012345", + "count" : 50, + "expectedBucket" : 6 + }, + { + "key" : "1tdwU5K8evYzMovqjExpBA", + "seed" : "-123456789012345", + "count" : 100, + "expectedBucket" : 6 + }, + { + "key" : "1tdwU5K8evYzMovqjExpBA", + "seed" : "8589934591", + "count" : 10, + "expectedBucket" : 4 + }, + { + "key" : "1tdwU5K8evYzMovqjExpBA", + "seed" : "8589934591", + "count" : 50, + "expectedBucket" : 34 + }, + { + "key" : "1tdwU5K8evYzMovqjExpBA", + "seed" : "8589934591", + "count" : 100, + "expectedBucket" : 84 + }, + { + "key" : "1tdwU5K8evYzMovqjExpBA", + "seed" : "-4294967295", + "count" : 10, + "expectedBucket" : 6 + }, + { + "key" : "1tdwU5K8evYzMovqjExpBA", + "seed" : "-4294967295", + "count" : 50, + "expectedBucket" : 36 + }, + { + "key" : "1tdwU5K8evYzMovqjExpBA", + "seed" : "-4294967295", + "count" : 100, + "expectedBucket" : 36 + }, + { + "key" : "1tdwU5K8evYzMovqjExpBA", + "seed" : "-9223372036854775808", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "1tdwU5K8evYzMovqjExpBA", + "seed" : "-9223372036854775808", + "count" : 50, + "expectedBucket" : 27 + }, + { + "key" : "1tdwU5K8evYzMovqjExpBA", + "seed" : "-9223372036854775808", + "count" : 100, + "expectedBucket" : 27 + }, + { + "key" : "1tdwU5K8evYzMovqjExpBA", + "seed" : "9223372036854775807", + "count" : 10, + "expectedBucket" : 6 + }, + { + "key" : "1tdwU5K8evYzMovqjExpBA", + "seed" : "9223372036854775807", + "count" : 50, + "expectedBucket" : 6 + }, + { + "key" : "1tdwU5K8evYzMovqjExpBA", + "seed" : "9223372036854775807", + "count" : 100, + "expectedBucket" : 56 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "0", + "count" : 10, + "expectedBucket" : 1 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "0", + "count" : 50, + "expectedBucket" : 1 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "0", + "count" : 100, + "expectedBucket" : 1 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "1", + "count" : 10, + "expectedBucket" : 9 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "1", + "count" : 50, + "expectedBucket" : 39 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "1", + "count" : 100, + "expectedBucket" : 39 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "-1", + "count" : 10, + "expectedBucket" : 8 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "-1", + "count" : 50, + "expectedBucket" : 18 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "-1", + "count" : 100, + "expectedBucket" : 68 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "42", + "count" : 10, + "expectedBucket" : 5 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "42", + "count" : 50, + "expectedBucket" : 5 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "42", + "count" : 100, + "expectedBucket" : 55 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "-123456789012345", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "-123456789012345", + "count" : 50, + "expectedBucket" : 10 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "-123456789012345", + "count" : 100, + "expectedBucket" : 60 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "8589934591", + "count" : 10, + "expectedBucket" : 5 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "8589934591", + "count" : 50, + "expectedBucket" : 5 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "8589934591", + "count" : 100, + "expectedBucket" : 55 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "-4294967295", + "count" : 10, + "expectedBucket" : 1 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "-4294967295", + "count" : 50, + "expectedBucket" : 21 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "-4294967295", + "count" : 100, + "expectedBucket" : 21 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "-9223372036854775808", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "-9223372036854775808", + "count" : 50, + "expectedBucket" : 27 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "-9223372036854775808", + "count" : 100, + "expectedBucket" : 27 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "9223372036854775807", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "9223372036854775807", + "count" : 50, + "expectedBucket" : 0 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "9223372036854775807", + "count" : 100, + "expectedBucket" : 50 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "0", + "count" : 10, + "expectedBucket" : 1 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "0", + "count" : 50, + "expectedBucket" : 1 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "0", + "count" : 100, + "expectedBucket" : 1 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "1", + "count" : 10, + "expectedBucket" : 9 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "1", + "count" : 50, + "expectedBucket" : 39 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "1", + "count" : 100, + "expectedBucket" : 39 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "-1", + "count" : 10, + "expectedBucket" : 8 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "-1", + "count" : 50, + "expectedBucket" : 18 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "-1", + "count" : 100, + "expectedBucket" : 68 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "42", + "count" : 10, + "expectedBucket" : 5 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "42", + "count" : 50, + "expectedBucket" : 5 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "42", + "count" : 100, + "expectedBucket" : 55 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "-123456789012345", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "-123456789012345", + "count" : 50, + "expectedBucket" : 10 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "-123456789012345", + "count" : 100, + "expectedBucket" : 60 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "8589934591", + "count" : 10, + "expectedBucket" : 5 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "8589934591", + "count" : 50, + "expectedBucket" : 5 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "8589934591", + "count" : 100, + "expectedBucket" : 55 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "-4294967295", + "count" : 10, + "expectedBucket" : 1 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "-4294967295", + "count" : 50, + "expectedBucket" : 21 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "-4294967295", + "count" : 100, + "expectedBucket" : 21 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "-9223372036854775808", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "-9223372036854775808", + "count" : 50, + "expectedBucket" : 27 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "-9223372036854775808", + "count" : 100, + "expectedBucket" : 27 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "9223372036854775807", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "9223372036854775807", + "count" : 50, + "expectedBucket" : 0 + }, + { + "key" : "ICy5YqxZB1uWSwcVLSNLcA", + "seed" : "9223372036854775807", + "count" : 100, + "expectedBucket" : 50 + }, + { + "key" : "z80ghJXVZe9m59_5-Ydk2g", + "seed" : "0", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : "z80ghJXVZe9m59_5-Ydk2g", + "seed" : "0", + "count" : 50, + "expectedBucket" : 40 + }, + { + "key" : "z80ghJXVZe9m59_5-Ydk2g", + "seed" : "0", + "count" : 100, + "expectedBucket" : 90 + }, + { + "key" : "z80ghJXVZe9m59_5-Ydk2g", + "seed" : "1", + "count" : 10, + "expectedBucket" : 8 + }, + { + "key" : "z80ghJXVZe9m59_5-Ydk2g", + "seed" : "1", + "count" : 50, + "expectedBucket" : 28 + }, + { + "key" : "z80ghJXVZe9m59_5-Ydk2g", + "seed" : "1", + "count" : 100, + "expectedBucket" : 78 + }, + { + "key" : "z80ghJXVZe9m59_5-Ydk2g", + "seed" : "-1", + "count" : 10, + "expectedBucket" : 8 + }, + { + "key" : "z80ghJXVZe9m59_5-Ydk2g", + "seed" : "-1", + "count" : 50, + "expectedBucket" : 28 + }, + { + "key" : "z80ghJXVZe9m59_5-Ydk2g", + "seed" : "-1", + "count" : 100, + "expectedBucket" : 78 + }, + { + "key" : "z80ghJXVZe9m59_5-Ydk2g", + "seed" : "42", + "count" : 10, + "expectedBucket" : 6 + }, + { + "key" : "z80ghJXVZe9m59_5-Ydk2g", + "seed" : "42", + "count" : 50, + "expectedBucket" : 36 + }, + { + "key" : "z80ghJXVZe9m59_5-Ydk2g", + "seed" : "42", + "count" : 100, + "expectedBucket" : 36 + }, + { + "key" : "z80ghJXVZe9m59_5-Ydk2g", + "seed" : "-123456789012345", + "count" : 10, + "expectedBucket" : 4 + }, + { + "key" : "z80ghJXVZe9m59_5-Ydk2g", + "seed" : "-123456789012345", + "count" : 50, + "expectedBucket" : 14 + }, + { + "key" : "z80ghJXVZe9m59_5-Ydk2g", + "seed" : "-123456789012345", + "count" : 100, + "expectedBucket" : 14 + }, + { + "key" : "z80ghJXVZe9m59_5-Ydk2g", + "seed" : "8589934591", + "count" : 10, + "expectedBucket" : 3 + }, + { + "key" : "z80ghJXVZe9m59_5-Ydk2g", + "seed" : "8589934591", + "count" : 50, + "expectedBucket" : 13 + }, + { + "key" : "z80ghJXVZe9m59_5-Ydk2g", + "seed" : "8589934591", + "count" : 100, + "expectedBucket" : 13 + }, + { + "key" : "z80ghJXVZe9m59_5-Ydk2g", + "seed" : "-4294967295", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : "z80ghJXVZe9m59_5-Ydk2g", + "seed" : "-4294967295", + "count" : 50, + "expectedBucket" : 40 + }, + { + "key" : "z80ghJXVZe9m59_5-Ydk2g", + "seed" : "-4294967295", + "count" : 100, + "expectedBucket" : 90 + }, + { + "key" : "z80ghJXVZe9m59_5-Ydk2g", + "seed" : "-9223372036854775808", + "count" : 10, + "expectedBucket" : 4 + }, + { + "key" : "z80ghJXVZe9m59_5-Ydk2g", + "seed" : "-9223372036854775808", + "count" : 50, + "expectedBucket" : 4 + }, + { + "key" : "z80ghJXVZe9m59_5-Ydk2g", + "seed" : "-9223372036854775808", + "count" : 100, + "expectedBucket" : 54 + }, + { + "key" : "z80ghJXVZe9m59_5-Ydk2g", + "seed" : "9223372036854775807", + "count" : 10, + "expectedBucket" : 3 + }, + { + "key" : "z80ghJXVZe9m59_5-Ydk2g", + "seed" : "9223372036854775807", + "count" : 50, + "expectedBucket" : 23 + }, + { + "key" : "z80ghJXVZe9m59_5-Ydk2g", + "seed" : "9223372036854775807", + "count" : 100, + "expectedBucket" : 23 + }, + { + "key" : "jfy4n9hiDj5_tqA6U_MH3A", + "seed" : "0", + "count" : 10, + "expectedBucket" : 8 + }, + { + "key" : "jfy4n9hiDj5_tqA6U_MH3A", + "seed" : "0", + "count" : 50, + "expectedBucket" : 8 + }, + { + "key" : "jfy4n9hiDj5_tqA6U_MH3A", + "seed" : "0", + "count" : 100, + "expectedBucket" : 58 + }, + { + "key" : "jfy4n9hiDj5_tqA6U_MH3A", + "seed" : "1", + "count" : 10, + "expectedBucket" : 6 + }, + { + "key" : "jfy4n9hiDj5_tqA6U_MH3A", + "seed" : "1", + "count" : 50, + "expectedBucket" : 26 + }, + { + "key" : "jfy4n9hiDj5_tqA6U_MH3A", + "seed" : "1", + "count" : 100, + "expectedBucket" : 26 + }, + { + "key" : "jfy4n9hiDj5_tqA6U_MH3A", + "seed" : "-1", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "jfy4n9hiDj5_tqA6U_MH3A", + "seed" : "-1", + "count" : 50, + "expectedBucket" : 47 + }, + { + "key" : "jfy4n9hiDj5_tqA6U_MH3A", + "seed" : "-1", + "count" : 100, + "expectedBucket" : 47 + }, + { + "key" : "jfy4n9hiDj5_tqA6U_MH3A", + "seed" : "42", + "count" : 10, + "expectedBucket" : 9 + }, + { + "key" : "jfy4n9hiDj5_tqA6U_MH3A", + "seed" : "42", + "count" : 50, + "expectedBucket" : 19 + }, + { + "key" : "jfy4n9hiDj5_tqA6U_MH3A", + "seed" : "42", + "count" : 100, + "expectedBucket" : 69 + }, + { + "key" : "jfy4n9hiDj5_tqA6U_MH3A", + "seed" : "-123456789012345", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : "jfy4n9hiDj5_tqA6U_MH3A", + "seed" : "-123456789012345", + "count" : 50, + "expectedBucket" : 20 + }, + { + "key" : "jfy4n9hiDj5_tqA6U_MH3A", + "seed" : "-123456789012345", + "count" : 100, + "expectedBucket" : 20 + }, + { + "key" : "jfy4n9hiDj5_tqA6U_MH3A", + "seed" : "8589934591", + "count" : 10, + "expectedBucket" : 2 + }, + { + "key" : "jfy4n9hiDj5_tqA6U_MH3A", + "seed" : "8589934591", + "count" : 50, + "expectedBucket" : 32 + }, + { + "key" : "jfy4n9hiDj5_tqA6U_MH3A", + "seed" : "8589934591", + "count" : 100, + "expectedBucket" : 32 + }, + { + "key" : "jfy4n9hiDj5_tqA6U_MH3A", + "seed" : "-4294967295", + "count" : 10, + "expectedBucket" : 9 + }, + { + "key" : "jfy4n9hiDj5_tqA6U_MH3A", + "seed" : "-4294967295", + "count" : 50, + "expectedBucket" : 39 + }, + { + "key" : "jfy4n9hiDj5_tqA6U_MH3A", + "seed" : "-4294967295", + "count" : 100, + "expectedBucket" : 39 + }, + { + "key" : "jfy4n9hiDj5_tqA6U_MH3A", + "seed" : "-9223372036854775808", + "count" : 10, + "expectedBucket" : 6 + }, + { + "key" : "jfy4n9hiDj5_tqA6U_MH3A", + "seed" : "-9223372036854775808", + "count" : 50, + "expectedBucket" : 46 + }, + { + "key" : "jfy4n9hiDj5_tqA6U_MH3A", + "seed" : "-9223372036854775808", + "count" : 100, + "expectedBucket" : 96 + }, + { + "key" : "jfy4n9hiDj5_tqA6U_MH3A", + "seed" : "9223372036854775807", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "jfy4n9hiDj5_tqA6U_MH3A", + "seed" : "9223372036854775807", + "count" : 50, + "expectedBucket" : 7 + }, + { + "key" : "jfy4n9hiDj5_tqA6U_MH3A", + "seed" : "9223372036854775807", + "count" : 100, + "expectedBucket" : 57 + }, + { + "key" : "i_qa0JD7v4flGPHOJKk-7g", + "seed" : "0", + "count" : 10, + "expectedBucket" : 6 + }, + { + "key" : "i_qa0JD7v4flGPHOJKk-7g", + "seed" : "0", + "count" : 50, + "expectedBucket" : 36 + }, + { + "key" : "i_qa0JD7v4flGPHOJKk-7g", + "seed" : "0", + "count" : 100, + "expectedBucket" : 86 + }, + { + "key" : "i_qa0JD7v4flGPHOJKk-7g", + "seed" : "1", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : "i_qa0JD7v4flGPHOJKk-7g", + "seed" : "1", + "count" : 50, + "expectedBucket" : 20 + }, + { + "key" : "i_qa0JD7v4flGPHOJKk-7g", + "seed" : "1", + "count" : 100, + "expectedBucket" : 70 + }, + { + "key" : "i_qa0JD7v4flGPHOJKk-7g", + "seed" : "-1", + "count" : 10, + "expectedBucket" : 3 + }, + { + "key" : "i_qa0JD7v4flGPHOJKk-7g", + "seed" : "-1", + "count" : 50, + "expectedBucket" : 43 + }, + { + "key" : "i_qa0JD7v4flGPHOJKk-7g", + "seed" : "-1", + "count" : 100, + "expectedBucket" : 43 + }, + { + "key" : "i_qa0JD7v4flGPHOJKk-7g", + "seed" : "42", + "count" : 10, + "expectedBucket" : 3 + }, + { + "key" : "i_qa0JD7v4flGPHOJKk-7g", + "seed" : "42", + "count" : 50, + "expectedBucket" : 23 + }, + { + "key" : "i_qa0JD7v4flGPHOJKk-7g", + "seed" : "42", + "count" : 100, + "expectedBucket" : 23 + }, + { + "key" : "i_qa0JD7v4flGPHOJKk-7g", + "seed" : "-123456789012345", + "count" : 10, + "expectedBucket" : 4 + }, + { + "key" : "i_qa0JD7v4flGPHOJKk-7g", + "seed" : "-123456789012345", + "count" : 50, + "expectedBucket" : 4 + }, + { + "key" : "i_qa0JD7v4flGPHOJKk-7g", + "seed" : "-123456789012345", + "count" : 100, + "expectedBucket" : 54 + }, + { + "key" : "i_qa0JD7v4flGPHOJKk-7g", + "seed" : "8589934591", + "count" : 10, + "expectedBucket" : 4 + }, + { + "key" : "i_qa0JD7v4flGPHOJKk-7g", + "seed" : "8589934591", + "count" : 50, + "expectedBucket" : 34 + }, + { + "key" : "i_qa0JD7v4flGPHOJKk-7g", + "seed" : "8589934591", + "count" : 100, + "expectedBucket" : 34 + }, + { + "key" : "i_qa0JD7v4flGPHOJKk-7g", + "seed" : "-4294967295", + "count" : 10, + "expectedBucket" : 1 + }, + { + "key" : "i_qa0JD7v4flGPHOJKk-7g", + "seed" : "-4294967295", + "count" : 50, + "expectedBucket" : 21 + }, + { + "key" : "i_qa0JD7v4flGPHOJKk-7g", + "seed" : "-4294967295", + "count" : 100, + "expectedBucket" : 71 + }, + { + "key" : "i_qa0JD7v4flGPHOJKk-7g", + "seed" : "-9223372036854775808", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "i_qa0JD7v4flGPHOJKk-7g", + "seed" : "-9223372036854775808", + "count" : 50, + "expectedBucket" : 47 + }, + { + "key" : "i_qa0JD7v4flGPHOJKk-7g", + "seed" : "-9223372036854775808", + "count" : 100, + "expectedBucket" : 97 + }, + { + "key" : "i_qa0JD7v4flGPHOJKk-7g", + "seed" : "9223372036854775807", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "i_qa0JD7v4flGPHOJKk-7g", + "seed" : "9223372036854775807", + "count" : 50, + "expectedBucket" : 37 + }, + { + "key" : "i_qa0JD7v4flGPHOJKk-7g", + "seed" : "9223372036854775807", + "count" : 100, + "expectedBucket" : 87 + }, + { + "key" : "1B2M2Y8AsgTpgAmY7PhCfg", + "seed" : "0", + "count" : 10, + "expectedBucket" : 9 + }, + { + "key" : "1B2M2Y8AsgTpgAmY7PhCfg", + "seed" : "0", + "count" : 50, + "expectedBucket" : 9 + }, + { + "key" : "1B2M2Y8AsgTpgAmY7PhCfg", + "seed" : "0", + "count" : 100, + "expectedBucket" : 9 + }, + { + "key" : "1B2M2Y8AsgTpgAmY7PhCfg", + "seed" : "1", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "1B2M2Y8AsgTpgAmY7PhCfg", + "seed" : "1", + "count" : 50, + "expectedBucket" : 37 + }, + { + "key" : "1B2M2Y8AsgTpgAmY7PhCfg", + "seed" : "1", + "count" : 100, + "expectedBucket" : 37 + }, + { + "key" : "1B2M2Y8AsgTpgAmY7PhCfg", + "seed" : "-1", + "count" : 10, + "expectedBucket" : 8 + }, + { + "key" : "1B2M2Y8AsgTpgAmY7PhCfg", + "seed" : "-1", + "count" : 50, + "expectedBucket" : 38 + }, + { + "key" : "1B2M2Y8AsgTpgAmY7PhCfg", + "seed" : "-1", + "count" : 100, + "expectedBucket" : 88 + }, + { + "key" : "1B2M2Y8AsgTpgAmY7PhCfg", + "seed" : "42", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : "1B2M2Y8AsgTpgAmY7PhCfg", + "seed" : "42", + "count" : 50, + "expectedBucket" : 30 + }, + { + "key" : "1B2M2Y8AsgTpgAmY7PhCfg", + "seed" : "42", + "count" : 100, + "expectedBucket" : 30 + }, + { + "key" : "1B2M2Y8AsgTpgAmY7PhCfg", + "seed" : "-123456789012345", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : "1B2M2Y8AsgTpgAmY7PhCfg", + "seed" : "-123456789012345", + "count" : 50, + "expectedBucket" : 20 + }, + { + "key" : "1B2M2Y8AsgTpgAmY7PhCfg", + "seed" : "-123456789012345", + "count" : 100, + "expectedBucket" : 70 + }, + { + "key" : "1B2M2Y8AsgTpgAmY7PhCfg", + "seed" : "8589934591", + "count" : 10, + "expectedBucket" : 9 + }, + { + "key" : "1B2M2Y8AsgTpgAmY7PhCfg", + "seed" : "8589934591", + "count" : 50, + "expectedBucket" : 49 + }, + { + "key" : "1B2M2Y8AsgTpgAmY7PhCfg", + "seed" : "8589934591", + "count" : 100, + "expectedBucket" : 49 + }, + { + "key" : "1B2M2Y8AsgTpgAmY7PhCfg", + "seed" : "-4294967295", + "count" : 10, + "expectedBucket" : 2 + }, + { + "key" : "1B2M2Y8AsgTpgAmY7PhCfg", + "seed" : "-4294967295", + "count" : 50, + "expectedBucket" : 22 + }, + { + "key" : "1B2M2Y8AsgTpgAmY7PhCfg", + "seed" : "-4294967295", + "count" : 100, + "expectedBucket" : 22 + }, + { + "key" : "1B2M2Y8AsgTpgAmY7PhCfg", + "seed" : "-9223372036854775808", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "1B2M2Y8AsgTpgAmY7PhCfg", + "seed" : "-9223372036854775808", + "count" : 50, + "expectedBucket" : 37 + }, + { + "key" : "1B2M2Y8AsgTpgAmY7PhCfg", + "seed" : "-9223372036854775808", + "count" : 100, + "expectedBucket" : 37 + }, + { + "key" : "1B2M2Y8AsgTpgAmY7PhCfg", + "seed" : "9223372036854775807", + "count" : 10, + "expectedBucket" : 9 + }, + { + "key" : "1B2M2Y8AsgTpgAmY7PhCfg", + "seed" : "9223372036854775807", + "count" : 50, + "expectedBucket" : 9 + }, + { + "key" : "1B2M2Y8AsgTpgAmY7PhCfg", + "seed" : "9223372036854775807", + "count" : 100, + "expectedBucket" : 9 + }, + { + "key" : "6nbArp3YF-tEj9Gz22JTuw", + "seed" : "0", + "count" : 10, + "expectedBucket" : 2 + }, + { + "key" : "6nbArp3YF-tEj9Gz22JTuw", + "seed" : "0", + "count" : 50, + "expectedBucket" : 2 + }, + { + "key" : "6nbArp3YF-tEj9Gz22JTuw", + "seed" : "0", + "count" : 100, + "expectedBucket" : 52 + }, + { + "key" : "6nbArp3YF-tEj9Gz22JTuw", + "seed" : "1", + "count" : 10, + "expectedBucket" : 3 + }, + { + "key" : "6nbArp3YF-tEj9Gz22JTuw", + "seed" : "1", + "count" : 50, + "expectedBucket" : 43 + }, + { + "key" : "6nbArp3YF-tEj9Gz22JTuw", + "seed" : "1", + "count" : 100, + "expectedBucket" : 93 + }, + { + "key" : "6nbArp3YF-tEj9Gz22JTuw", + "seed" : "-1", + "count" : 10, + "expectedBucket" : 4 + }, + { + "key" : "6nbArp3YF-tEj9Gz22JTuw", + "seed" : "-1", + "count" : 50, + "expectedBucket" : 34 + }, + { + "key" : "6nbArp3YF-tEj9Gz22JTuw", + "seed" : "-1", + "count" : 100, + "expectedBucket" : 34 + }, + { + "key" : "6nbArp3YF-tEj9Gz22JTuw", + "seed" : "42", + "count" : 10, + "expectedBucket" : 9 + }, + { + "key" : "6nbArp3YF-tEj9Gz22JTuw", + "seed" : "42", + "count" : 50, + "expectedBucket" : 39 + }, + { + "key" : "6nbArp3YF-tEj9Gz22JTuw", + "seed" : "42", + "count" : 100, + "expectedBucket" : 39 + }, + { + "key" : "6nbArp3YF-tEj9Gz22JTuw", + "seed" : "-123456789012345", + "count" : 10, + "expectedBucket" : 4 + }, + { + "key" : "6nbArp3YF-tEj9Gz22JTuw", + "seed" : "-123456789012345", + "count" : 50, + "expectedBucket" : 44 + }, + { + "key" : "6nbArp3YF-tEj9Gz22JTuw", + "seed" : "-123456789012345", + "count" : 100, + "expectedBucket" : 94 + }, + { + "key" : "6nbArp3YF-tEj9Gz22JTuw", + "seed" : "8589934591", + "count" : 10, + "expectedBucket" : 1 + }, + { + "key" : "6nbArp3YF-tEj9Gz22JTuw", + "seed" : "8589934591", + "count" : 50, + "expectedBucket" : 11 + }, + { + "key" : "6nbArp3YF-tEj9Gz22JTuw", + "seed" : "8589934591", + "count" : 100, + "expectedBucket" : 11 + }, + { + "key" : "6nbArp3YF-tEj9Gz22JTuw", + "seed" : "-4294967295", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "6nbArp3YF-tEj9Gz22JTuw", + "seed" : "-4294967295", + "count" : 50, + "expectedBucket" : 47 + }, + { + "key" : "6nbArp3YF-tEj9Gz22JTuw", + "seed" : "-4294967295", + "count" : 100, + "expectedBucket" : 47 + }, + { + "key" : "6nbArp3YF-tEj9Gz22JTuw", + "seed" : "-9223372036854775808", + "count" : 10, + "expectedBucket" : 6 + }, + { + "key" : "6nbArp3YF-tEj9Gz22JTuw", + "seed" : "-9223372036854775808", + "count" : 50, + "expectedBucket" : 16 + }, + { + "key" : "6nbArp3YF-tEj9Gz22JTuw", + "seed" : "-9223372036854775808", + "count" : 100, + "expectedBucket" : 66 + }, + { + "key" : "6nbArp3YF-tEj9Gz22JTuw", + "seed" : "9223372036854775807", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "6nbArp3YF-tEj9Gz22JTuw", + "seed" : "9223372036854775807", + "count" : 50, + "expectedBucket" : 27 + }, + { + "key" : "6nbArp3YF-tEj9Gz22JTuw", + "seed" : "9223372036854775807", + "count" : 100, + "expectedBucket" : 77 + }, + { + "key" : "cHuhfH742e8Is57zFK30Mg", + "seed" : "0", + "count" : 10, + "expectedBucket" : 2 + }, + { + "key" : "cHuhfH742e8Is57zFK30Mg", + "seed" : "0", + "count" : 50, + "expectedBucket" : 32 + }, + { + "key" : "cHuhfH742e8Is57zFK30Mg", + "seed" : "0", + "count" : 100, + "expectedBucket" : 82 + }, + { + "key" : "cHuhfH742e8Is57zFK30Mg", + "seed" : "1", + "count" : 10, + "expectedBucket" : 6 + }, + { + "key" : "cHuhfH742e8Is57zFK30Mg", + "seed" : "1", + "count" : 50, + "expectedBucket" : 36 + }, + { + "key" : "cHuhfH742e8Is57zFK30Mg", + "seed" : "1", + "count" : 100, + "expectedBucket" : 36 + }, + { + "key" : "cHuhfH742e8Is57zFK30Mg", + "seed" : "-1", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : "cHuhfH742e8Is57zFK30Mg", + "seed" : "-1", + "count" : 50, + "expectedBucket" : 40 + }, + { + "key" : "cHuhfH742e8Is57zFK30Mg", + "seed" : "-1", + "count" : 100, + "expectedBucket" : 90 + }, + { + "key" : "cHuhfH742e8Is57zFK30Mg", + "seed" : "42", + "count" : 10, + "expectedBucket" : 6 + }, + { + "key" : "cHuhfH742e8Is57zFK30Mg", + "seed" : "42", + "count" : 50, + "expectedBucket" : 16 + }, + { + "key" : "cHuhfH742e8Is57zFK30Mg", + "seed" : "42", + "count" : 100, + "expectedBucket" : 16 + }, + { + "key" : "cHuhfH742e8Is57zFK30Mg", + "seed" : "-123456789012345", + "count" : 10, + "expectedBucket" : 9 + }, + { + "key" : "cHuhfH742e8Is57zFK30Mg", + "seed" : "-123456789012345", + "count" : 50, + "expectedBucket" : 29 + }, + { + "key" : "cHuhfH742e8Is57zFK30Mg", + "seed" : "-123456789012345", + "count" : 100, + "expectedBucket" : 29 + }, + { + "key" : "cHuhfH742e8Is57zFK30Mg", + "seed" : "8589934591", + "count" : 10, + "expectedBucket" : 2 + }, + { + "key" : "cHuhfH742e8Is57zFK30Mg", + "seed" : "8589934591", + "count" : 50, + "expectedBucket" : 32 + }, + { + "key" : "cHuhfH742e8Is57zFK30Mg", + "seed" : "8589934591", + "count" : 100, + "expectedBucket" : 32 + }, + { + "key" : "cHuhfH742e8Is57zFK30Mg", + "seed" : "-4294967295", + "count" : 10, + "expectedBucket" : 5 + }, + { + "key" : "cHuhfH742e8Is57zFK30Mg", + "seed" : "-4294967295", + "count" : 50, + "expectedBucket" : 25 + }, + { + "key" : "cHuhfH742e8Is57zFK30Mg", + "seed" : "-4294967295", + "count" : 100, + "expectedBucket" : 75 + }, + { + "key" : "cHuhfH742e8Is57zFK30Mg", + "seed" : "-9223372036854775808", + "count" : 10, + "expectedBucket" : 5 + }, + { + "key" : "cHuhfH742e8Is57zFK30Mg", + "seed" : "-9223372036854775808", + "count" : 50, + "expectedBucket" : 35 + }, + { + "key" : "cHuhfH742e8Is57zFK30Mg", + "seed" : "-9223372036854775808", + "count" : 100, + "expectedBucket" : 35 + }, + { + "key" : "cHuhfH742e8Is57zFK30Mg", + "seed" : "9223372036854775807", + "count" : 10, + "expectedBucket" : 2 + }, + { + "key" : "cHuhfH742e8Is57zFK30Mg", + "seed" : "9223372036854775807", + "count" : 50, + "expectedBucket" : 42 + }, + { + "key" : "cHuhfH742e8Is57zFK30Mg", + "seed" : "9223372036854775807", + "count" : 100, + "expectedBucket" : 92 + }, + { + "key" : "KgLqw51xanDs83V5GFkntg", + "seed" : "0", + "count" : 10, + "expectedBucket" : 8 + }, + { + "key" : "KgLqw51xanDs83V5GFkntg", + "seed" : "0", + "count" : 50, + "expectedBucket" : 28 + }, + { + "key" : "KgLqw51xanDs83V5GFkntg", + "seed" : "0", + "count" : 100, + "expectedBucket" : 28 + }, + { + "key" : "KgLqw51xanDs83V5GFkntg", + "seed" : "1", + "count" : 10, + "expectedBucket" : 9 + }, + { + "key" : "KgLqw51xanDs83V5GFkntg", + "seed" : "1", + "count" : 50, + "expectedBucket" : 49 + }, + { + "key" : "KgLqw51xanDs83V5GFkntg", + "seed" : "1", + "count" : 100, + "expectedBucket" : 49 + }, + { + "key" : "KgLqw51xanDs83V5GFkntg", + "seed" : "-1", + "count" : 10, + "expectedBucket" : 2 + }, + { + "key" : "KgLqw51xanDs83V5GFkntg", + "seed" : "-1", + "count" : 50, + "expectedBucket" : 42 + }, + { + "key" : "KgLqw51xanDs83V5GFkntg", + "seed" : "-1", + "count" : 100, + "expectedBucket" : 42 + }, + { + "key" : "KgLqw51xanDs83V5GFkntg", + "seed" : "42", + "count" : 10, + "expectedBucket" : 6 + }, + { + "key" : "KgLqw51xanDs83V5GFkntg", + "seed" : "42", + "count" : 50, + "expectedBucket" : 16 + }, + { + "key" : "KgLqw51xanDs83V5GFkntg", + "seed" : "42", + "count" : 100, + "expectedBucket" : 66 + }, + { + "key" : "KgLqw51xanDs83V5GFkntg", + "seed" : "-123456789012345", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "KgLqw51xanDs83V5GFkntg", + "seed" : "-123456789012345", + "count" : 50, + "expectedBucket" : 37 + }, + { + "key" : "KgLqw51xanDs83V5GFkntg", + "seed" : "-123456789012345", + "count" : 100, + "expectedBucket" : 87 + }, + { + "key" : "KgLqw51xanDs83V5GFkntg", + "seed" : "8589934591", + "count" : 10, + "expectedBucket" : 1 + }, + { + "key" : "KgLqw51xanDs83V5GFkntg", + "seed" : "8589934591", + "count" : 50, + "expectedBucket" : 11 + }, + { + "key" : "KgLqw51xanDs83V5GFkntg", + "seed" : "8589934591", + "count" : 100, + "expectedBucket" : 61 + }, + { + "key" : "KgLqw51xanDs83V5GFkntg", + "seed" : "-4294967295", + "count" : 10, + "expectedBucket" : 2 + }, + { + "key" : "KgLqw51xanDs83V5GFkntg", + "seed" : "-4294967295", + "count" : 50, + "expectedBucket" : 32 + }, + { + "key" : "KgLqw51xanDs83V5GFkntg", + "seed" : "-4294967295", + "count" : 100, + "expectedBucket" : 32 + }, + { + "key" : "KgLqw51xanDs83V5GFkntg", + "seed" : "-9223372036854775808", + "count" : 10, + "expectedBucket" : 3 + }, + { + "key" : "KgLqw51xanDs83V5GFkntg", + "seed" : "-9223372036854775808", + "count" : 50, + "expectedBucket" : 23 + }, + { + "key" : "KgLqw51xanDs83V5GFkntg", + "seed" : "-9223372036854775808", + "count" : 100, + "expectedBucket" : 23 + }, + { + "key" : "KgLqw51xanDs83V5GFkntg", + "seed" : "9223372036854775807", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "KgLqw51xanDs83V5GFkntg", + "seed" : "9223372036854775807", + "count" : 50, + "expectedBucket" : 37 + }, + { + "key" : "KgLqw51xanDs83V5GFkntg", + "seed" : "9223372036854775807", + "count" : 100, + "expectedBucket" : 37 + }, + { + "key" : "3eFBzFDWEYpd2q4aAbdPmQ", + "seed" : "0", + "count" : 10, + "expectedBucket" : 5 + }, + { + "key" : "3eFBzFDWEYpd2q4aAbdPmQ", + "seed" : "0", + "count" : 50, + "expectedBucket" : 35 + }, + { + "key" : "3eFBzFDWEYpd2q4aAbdPmQ", + "seed" : "0", + "count" : 100, + "expectedBucket" : 35 + }, + { + "key" : "3eFBzFDWEYpd2q4aAbdPmQ", + "seed" : "1", + "count" : 10, + "expectedBucket" : 2 + }, + { + "key" : "3eFBzFDWEYpd2q4aAbdPmQ", + "seed" : "1", + "count" : 50, + "expectedBucket" : 2 + }, + { + "key" : "3eFBzFDWEYpd2q4aAbdPmQ", + "seed" : "1", + "count" : 100, + "expectedBucket" : 2 + }, + { + "key" : "3eFBzFDWEYpd2q4aAbdPmQ", + "seed" : "-1", + "count" : 10, + "expectedBucket" : 2 + }, + { + "key" : "3eFBzFDWEYpd2q4aAbdPmQ", + "seed" : "-1", + "count" : 50, + "expectedBucket" : 22 + }, + { + "key" : "3eFBzFDWEYpd2q4aAbdPmQ", + "seed" : "-1", + "count" : 100, + "expectedBucket" : 72 + }, + { + "key" : "3eFBzFDWEYpd2q4aAbdPmQ", + "seed" : "42", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : "3eFBzFDWEYpd2q4aAbdPmQ", + "seed" : "42", + "count" : 50, + "expectedBucket" : 40 + }, + { + "key" : "3eFBzFDWEYpd2q4aAbdPmQ", + "seed" : "42", + "count" : 100, + "expectedBucket" : 40 + }, + { + "key" : "3eFBzFDWEYpd2q4aAbdPmQ", + "seed" : "-123456789012345", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "3eFBzFDWEYpd2q4aAbdPmQ", + "seed" : "-123456789012345", + "count" : 50, + "expectedBucket" : 17 + }, + { + "key" : "3eFBzFDWEYpd2q4aAbdPmQ", + "seed" : "-123456789012345", + "count" : 100, + "expectedBucket" : 17 + }, + { + "key" : "3eFBzFDWEYpd2q4aAbdPmQ", + "seed" : "8589934591", + "count" : 10, + "expectedBucket" : 3 + }, + { + "key" : "3eFBzFDWEYpd2q4aAbdPmQ", + "seed" : "8589934591", + "count" : 50, + "expectedBucket" : 3 + }, + { + "key" : "3eFBzFDWEYpd2q4aAbdPmQ", + "seed" : "8589934591", + "count" : 100, + "expectedBucket" : 3 + }, + { + "key" : "3eFBzFDWEYpd2q4aAbdPmQ", + "seed" : "-4294967295", + "count" : 10, + "expectedBucket" : 8 + }, + { + "key" : "3eFBzFDWEYpd2q4aAbdPmQ", + "seed" : "-4294967295", + "count" : 50, + "expectedBucket" : 18 + }, + { + "key" : "3eFBzFDWEYpd2q4aAbdPmQ", + "seed" : "-4294967295", + "count" : 100, + "expectedBucket" : 18 + }, + { + "key" : "3eFBzFDWEYpd2q4aAbdPmQ", + "seed" : "-9223372036854775808", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : "3eFBzFDWEYpd2q4aAbdPmQ", + "seed" : "-9223372036854775808", + "count" : 50, + "expectedBucket" : 40 + }, + { + "key" : "3eFBzFDWEYpd2q4aAbdPmQ", + "seed" : "-9223372036854775808", + "count" : 100, + "expectedBucket" : 40 + }, + { + "key" : "3eFBzFDWEYpd2q4aAbdPmQ", + "seed" : "9223372036854775807", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "3eFBzFDWEYpd2q4aAbdPmQ", + "seed" : "9223372036854775807", + "count" : 50, + "expectedBucket" : 17 + }, + { + "key" : "3eFBzFDWEYpd2q4aAbdPmQ", + "seed" : "9223372036854775807", + "count" : 100, + "expectedBucket" : 67 + }, + { + "key" : "WOAvK4WUCN3v173kQ-NR-g", + "seed" : "0", + "count" : 10, + "expectedBucket" : 8 + }, + { + "key" : "WOAvK4WUCN3v173kQ-NR-g", + "seed" : "0", + "count" : 50, + "expectedBucket" : 8 + }, + { + "key" : "WOAvK4WUCN3v173kQ-NR-g", + "seed" : "0", + "count" : 100, + "expectedBucket" : 8 + }, + { + "key" : "WOAvK4WUCN3v173kQ-NR-g", + "seed" : "1", + "count" : 10, + "expectedBucket" : 4 + }, + { + "key" : "WOAvK4WUCN3v173kQ-NR-g", + "seed" : "1", + "count" : 50, + "expectedBucket" : 24 + }, + { + "key" : "WOAvK4WUCN3v173kQ-NR-g", + "seed" : "1", + "count" : 100, + "expectedBucket" : 24 + }, + { + "key" : "WOAvK4WUCN3v173kQ-NR-g", + "seed" : "-1", + "count" : 10, + "expectedBucket" : 2 + }, + { + "key" : "WOAvK4WUCN3v173kQ-NR-g", + "seed" : "-1", + "count" : 50, + "expectedBucket" : 12 + }, + { + "key" : "WOAvK4WUCN3v173kQ-NR-g", + "seed" : "-1", + "count" : 100, + "expectedBucket" : 62 + }, + { + "key" : "WOAvK4WUCN3v173kQ-NR-g", + "seed" : "42", + "count" : 10, + "expectedBucket" : 8 + }, + { + "key" : "WOAvK4WUCN3v173kQ-NR-g", + "seed" : "42", + "count" : 50, + "expectedBucket" : 18 + }, + { + "key" : "WOAvK4WUCN3v173kQ-NR-g", + "seed" : "42", + "count" : 100, + "expectedBucket" : 18 + }, + { + "key" : "WOAvK4WUCN3v173kQ-NR-g", + "seed" : "-123456789012345", + "count" : 10, + "expectedBucket" : 8 + }, + { + "key" : "WOAvK4WUCN3v173kQ-NR-g", + "seed" : "-123456789012345", + "count" : 50, + "expectedBucket" : 8 + }, + { + "key" : "WOAvK4WUCN3v173kQ-NR-g", + "seed" : "-123456789012345", + "count" : 100, + "expectedBucket" : 58 + }, + { + "key" : "WOAvK4WUCN3v173kQ-NR-g", + "seed" : "8589934591", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : "WOAvK4WUCN3v173kQ-NR-g", + "seed" : "8589934591", + "count" : 50, + "expectedBucket" : 20 + }, + { + "key" : "WOAvK4WUCN3v173kQ-NR-g", + "seed" : "8589934591", + "count" : 100, + "expectedBucket" : 70 + }, + { + "key" : "WOAvK4WUCN3v173kQ-NR-g", + "seed" : "-4294967295", + "count" : 10, + "expectedBucket" : 6 + }, + { + "key" : "WOAvK4WUCN3v173kQ-NR-g", + "seed" : "-4294967295", + "count" : 50, + "expectedBucket" : 36 + }, + { + "key" : "WOAvK4WUCN3v173kQ-NR-g", + "seed" : "-4294967295", + "count" : 100, + "expectedBucket" : 86 + }, + { + "key" : "WOAvK4WUCN3v173kQ-NR-g", + "seed" : "-9223372036854775808", + "count" : 10, + "expectedBucket" : 1 + }, + { + "key" : "WOAvK4WUCN3v173kQ-NR-g", + "seed" : "-9223372036854775808", + "count" : 50, + "expectedBucket" : 31 + }, + { + "key" : "WOAvK4WUCN3v173kQ-NR-g", + "seed" : "-9223372036854775808", + "count" : 100, + "expectedBucket" : 31 + }, + { + "key" : "WOAvK4WUCN3v173kQ-NR-g", + "seed" : "9223372036854775807", + "count" : 10, + "expectedBucket" : 8 + }, + { + "key" : "WOAvK4WUCN3v173kQ-NR-g", + "seed" : "9223372036854775807", + "count" : 50, + "expectedBucket" : 28 + }, + { + "key" : "WOAvK4WUCN3v173kQ-NR-g", + "seed" : "9223372036854775807", + "count" : 100, + "expectedBucket" : 78 + }, + { + "key" : "Lisboa", + "seed" : "0", + "count" : 10, + "expectedBucket" : 6 + }, + { + "key" : "Lisboa", + "seed" : "0", + "count" : 50, + "expectedBucket" : 26 + }, + { + "key" : "Lisboa", + "seed" : "0", + "count" : 100, + "expectedBucket" : 76 + }, + { + "key" : "Lisboa", + "seed" : "1", + "count" : 10, + "expectedBucket" : 8 + }, + { + "key" : "Lisboa", + "seed" : "1", + "count" : 50, + "expectedBucket" : 8 + }, + { + "key" : "Lisboa", + "seed" : "1", + "count" : 100, + "expectedBucket" : 8 + }, + { + "key" : "Lisboa", + "seed" : "-1", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : "Lisboa", + "seed" : "-1", + "count" : 50, + "expectedBucket" : 0 + }, + { + "key" : "Lisboa", + "seed" : "-1", + "count" : 100, + "expectedBucket" : 0 + }, + { + "key" : "Lisboa", + "seed" : "42", + "count" : 10, + "expectedBucket" : 2 + }, + { + "key" : "Lisboa", + "seed" : "42", + "count" : 50, + "expectedBucket" : 12 + }, + { + "key" : "Lisboa", + "seed" : "42", + "count" : 100, + "expectedBucket" : 62 + }, + { + "key" : "Lisboa", + "seed" : "-123456789012345", + "count" : 10, + "expectedBucket" : 2 + }, + { + "key" : "Lisboa", + "seed" : "-123456789012345", + "count" : 50, + "expectedBucket" : 12 + }, + { + "key" : "Lisboa", + "seed" : "-123456789012345", + "count" : 100, + "expectedBucket" : 62 + }, + { + "key" : "Lisboa", + "seed" : "8589934591", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "Lisboa", + "seed" : "8589934591", + "count" : 50, + "expectedBucket" : 17 + }, + { + "key" : "Lisboa", + "seed" : "8589934591", + "count" : 100, + "expectedBucket" : 67 + }, + { + "key" : "Lisboa", + "seed" : "-4294967295", + "count" : 10, + "expectedBucket" : 3 + }, + { + "key" : "Lisboa", + "seed" : "-4294967295", + "count" : 50, + "expectedBucket" : 13 + }, + { + "key" : "Lisboa", + "seed" : "-4294967295", + "count" : 100, + "expectedBucket" : 63 + }, + { + "key" : "Lisboa", + "seed" : "-9223372036854775808", + "count" : 10, + "expectedBucket" : 5 + }, + { + "key" : "Lisboa", + "seed" : "-9223372036854775808", + "count" : 50, + "expectedBucket" : 45 + }, + { + "key" : "Lisboa", + "seed" : "-9223372036854775808", + "count" : 100, + "expectedBucket" : 45 + }, + { + "key" : "Lisboa", + "seed" : "9223372036854775807", + "count" : 10, + "expectedBucket" : 3 + }, + { + "key" : "Lisboa", + "seed" : "9223372036854775807", + "count" : 50, + "expectedBucket" : 23 + }, + { + "key" : "Lisboa", + "seed" : "9223372036854775807", + "count" : 100, + "expectedBucket" : 23 + }, + { + "key" : "São Paulo", + "seed" : "0", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : "São Paulo", + "seed" : "0", + "count" : 50, + "expectedBucket" : 0 + }, + { + "key" : "São Paulo", + "seed" : "0", + "count" : 100, + "expectedBucket" : 0 + }, + { + "key" : "São Paulo", + "seed" : "1", + "count" : 10, + "expectedBucket" : 9 + }, + { + "key" : "São Paulo", + "seed" : "1", + "count" : 50, + "expectedBucket" : 39 + }, + { + "key" : "São Paulo", + "seed" : "1", + "count" : 100, + "expectedBucket" : 39 + }, + { + "key" : "São Paulo", + "seed" : "-1", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "São Paulo", + "seed" : "-1", + "count" : 50, + "expectedBucket" : 17 + }, + { + "key" : "São Paulo", + "seed" : "-1", + "count" : 100, + "expectedBucket" : 67 + }, + { + "key" : "São Paulo", + "seed" : "42", + "count" : 10, + "expectedBucket" : 6 + }, + { + "key" : "São Paulo", + "seed" : "42", + "count" : 50, + "expectedBucket" : 26 + }, + { + "key" : "São Paulo", + "seed" : "42", + "count" : 100, + "expectedBucket" : 26 + }, + { + "key" : "São Paulo", + "seed" : "-123456789012345", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : "São Paulo", + "seed" : "-123456789012345", + "count" : 50, + "expectedBucket" : 10 + }, + { + "key" : "São Paulo", + "seed" : "-123456789012345", + "count" : 100, + "expectedBucket" : 60 + }, + { + "key" : "São Paulo", + "seed" : "8589934591", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "São Paulo", + "seed" : "8589934591", + "count" : 50, + "expectedBucket" : 37 + }, + { + "key" : "São Paulo", + "seed" : "8589934591", + "count" : 100, + "expectedBucket" : 87 + }, + { + "key" : "São Paulo", + "seed" : "-4294967295", + "count" : 10, + "expectedBucket" : 6 + }, + { + "key" : "São Paulo", + "seed" : "-4294967295", + "count" : 50, + "expectedBucket" : 46 + }, + { + "key" : "São Paulo", + "seed" : "-4294967295", + "count" : 100, + "expectedBucket" : 46 + }, + { + "key" : "São Paulo", + "seed" : "-9223372036854775808", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : "São Paulo", + "seed" : "-9223372036854775808", + "count" : 50, + "expectedBucket" : 30 + }, + { + "key" : "São Paulo", + "seed" : "-9223372036854775808", + "count" : 100, + "expectedBucket" : 80 + }, + { + "key" : "São Paulo", + "seed" : "9223372036854775807", + "count" : 10, + "expectedBucket" : 8 + }, + { + "key" : "São Paulo", + "seed" : "9223372036854775807", + "count" : 50, + "expectedBucket" : 38 + }, + { + "key" : "São Paulo", + "seed" : "9223372036854775807", + "count" : 100, + "expectedBucket" : 88 + }, + { + "key" : "東京", + "seed" : "0", + "count" : 10, + "expectedBucket" : 8 + }, + { + "key" : "東京", + "seed" : "0", + "count" : 50, + "expectedBucket" : 38 + }, + { + "key" : "東京", + "seed" : "0", + "count" : 100, + "expectedBucket" : 88 + }, + { + "key" : "東京", + "seed" : "1", + "count" : 10, + "expectedBucket" : 9 + }, + { + "key" : "東京", + "seed" : "1", + "count" : 50, + "expectedBucket" : 29 + }, + { + "key" : "東京", + "seed" : "1", + "count" : 100, + "expectedBucket" : 29 + }, + { + "key" : "東京", + "seed" : "-1", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : "東京", + "seed" : "-1", + "count" : 50, + "expectedBucket" : 20 + }, + { + "key" : "東京", + "seed" : "-1", + "count" : 100, + "expectedBucket" : 20 + }, + { + "key" : "東京", + "seed" : "42", + "count" : 10, + "expectedBucket" : 4 + }, + { + "key" : "東京", + "seed" : "42", + "count" : 50, + "expectedBucket" : 24 + }, + { + "key" : "東京", + "seed" : "42", + "count" : 100, + "expectedBucket" : 24 + }, + { + "key" : "東京", + "seed" : "-123456789012345", + "count" : 10, + "expectedBucket" : 9 + }, + { + "key" : "東京", + "seed" : "-123456789012345", + "count" : 50, + "expectedBucket" : 19 + }, + { + "key" : "東京", + "seed" : "-123456789012345", + "count" : 100, + "expectedBucket" : 19 + }, + { + "key" : "東京", + "seed" : "8589934591", + "count" : 10, + "expectedBucket" : 2 + }, + { + "key" : "東京", + "seed" : "8589934591", + "count" : 50, + "expectedBucket" : 42 + }, + { + "key" : "東京", + "seed" : "8589934591", + "count" : 100, + "expectedBucket" : 92 + }, + { + "key" : "東京", + "seed" : "-4294967295", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "東京", + "seed" : "-4294967295", + "count" : 50, + "expectedBucket" : 17 + }, + { + "key" : "東京", + "seed" : "-4294967295", + "count" : 100, + "expectedBucket" : 67 + }, + { + "key" : "東京", + "seed" : "-9223372036854775808", + "count" : 10, + "expectedBucket" : 2 + }, + { + "key" : "東京", + "seed" : "-9223372036854775808", + "count" : 50, + "expectedBucket" : 22 + }, + { + "key" : "東京", + "seed" : "-9223372036854775808", + "count" : 100, + "expectedBucket" : 72 + }, + { + "key" : "東京", + "seed" : "9223372036854775807", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : "東京", + "seed" : "9223372036854775807", + "count" : 50, + "expectedBucket" : 0 + }, + { + "key" : "東京", + "seed" : "9223372036854775807", + "count" : 100, + "expectedBucket" : 50 + }, + { + "key" : "😀", + "seed" : "0", + "count" : 10, + "expectedBucket" : 5 + }, + { + "key" : "😀", + "seed" : "0", + "count" : 50, + "expectedBucket" : 25 + }, + { + "key" : "😀", + "seed" : "0", + "count" : 100, + "expectedBucket" : 25 + }, + { + "key" : "😀", + "seed" : "1", + "count" : 10, + "expectedBucket" : 2 + }, + { + "key" : "😀", + "seed" : "1", + "count" : 50, + "expectedBucket" : 22 + }, + { + "key" : "😀", + "seed" : "1", + "count" : 100, + "expectedBucket" : 72 + }, + { + "key" : "😀", + "seed" : "-1", + "count" : 10, + "expectedBucket" : 9 + }, + { + "key" : "😀", + "seed" : "-1", + "count" : 50, + "expectedBucket" : 39 + }, + { + "key" : "😀", + "seed" : "-1", + "count" : 100, + "expectedBucket" : 39 + }, + { + "key" : "😀", + "seed" : "42", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "😀", + "seed" : "42", + "count" : 50, + "expectedBucket" : 37 + }, + { + "key" : "😀", + "seed" : "42", + "count" : 100, + "expectedBucket" : 37 + }, + { + "key" : "😀", + "seed" : "-123456789012345", + "count" : 10, + "expectedBucket" : 1 + }, + { + "key" : "😀", + "seed" : "-123456789012345", + "count" : 50, + "expectedBucket" : 11 + }, + { + "key" : "😀", + "seed" : "-123456789012345", + "count" : 100, + "expectedBucket" : 11 + }, + { + "key" : "😀", + "seed" : "8589934591", + "count" : 10, + "expectedBucket" : 2 + }, + { + "key" : "😀", + "seed" : "8589934591", + "count" : 50, + "expectedBucket" : 12 + }, + { + "key" : "😀", + "seed" : "8589934591", + "count" : 100, + "expectedBucket" : 62 + }, + { + "key" : "😀", + "seed" : "-4294967295", + "count" : 10, + "expectedBucket" : 2 + }, + { + "key" : "😀", + "seed" : "-4294967295", + "count" : 50, + "expectedBucket" : 42 + }, + { + "key" : "😀", + "seed" : "-4294967295", + "count" : 100, + "expectedBucket" : 92 + }, + { + "key" : "😀", + "seed" : "-9223372036854775808", + "count" : 10, + "expectedBucket" : 1 + }, + { + "key" : "😀", + "seed" : "-9223372036854775808", + "count" : 50, + "expectedBucket" : 1 + }, + { + "key" : "😀", + "seed" : "-9223372036854775808", + "count" : 100, + "expectedBucket" : 1 + }, + { + "key" : "😀", + "seed" : "9223372036854775807", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "😀", + "seed" : "9223372036854775807", + "count" : 50, + "expectedBucket" : 47 + }, + { + "key" : "😀", + "seed" : "9223372036854775807", + "count" : 100, + "expectedBucket" : 47 + }, + { + "key" : "42", + "seed" : "0", + "count" : 10, + "expectedBucket" : 2 + }, + { + "key" : "42", + "seed" : "0", + "count" : 50, + "expectedBucket" : 32 + }, + { + "key" : "42", + "seed" : "0", + "count" : 100, + "expectedBucket" : 82 + }, + { + "key" : "42", + "seed" : "1", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : "42", + "seed" : "1", + "count" : 50, + "expectedBucket" : 30 + }, + { + "key" : "42", + "seed" : "1", + "count" : 100, + "expectedBucket" : 30 + }, + { + "key" : "42", + "seed" : "-1", + "count" : 10, + "expectedBucket" : 9 + }, + { + "key" : "42", + "seed" : "-1", + "count" : 50, + "expectedBucket" : 39 + }, + { + "key" : "42", + "seed" : "-1", + "count" : 100, + "expectedBucket" : 39 + }, + { + "key" : "42", + "seed" : "42", + "count" : 10, + "expectedBucket" : 2 + }, + { + "key" : "42", + "seed" : "42", + "count" : 50, + "expectedBucket" : 22 + }, + { + "key" : "42", + "seed" : "42", + "count" : 100, + "expectedBucket" : 72 + }, + { + "key" : "42", + "seed" : "-123456789012345", + "count" : 10, + "expectedBucket" : 5 + }, + { + "key" : "42", + "seed" : "-123456789012345", + "count" : 50, + "expectedBucket" : 5 + }, + { + "key" : "42", + "seed" : "-123456789012345", + "count" : 100, + "expectedBucket" : 5 + }, + { + "key" : "42", + "seed" : "8589934591", + "count" : 10, + "expectedBucket" : 1 + }, + { + "key" : "42", + "seed" : "8589934591", + "count" : 50, + "expectedBucket" : 41 + }, + { + "key" : "42", + "seed" : "8589934591", + "count" : 100, + "expectedBucket" : 91 + }, + { + "key" : "42", + "seed" : "-4294967295", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : "42", + "seed" : "-4294967295", + "count" : 50, + "expectedBucket" : 10 + }, + { + "key" : "42", + "seed" : "-4294967295", + "count" : 100, + "expectedBucket" : 60 + }, + { + "key" : "42", + "seed" : "-9223372036854775808", + "count" : 10, + "expectedBucket" : 6 + }, + { + "key" : "42", + "seed" : "-9223372036854775808", + "count" : 50, + "expectedBucket" : 26 + }, + { + "key" : "42", + "seed" : "-9223372036854775808", + "count" : 100, + "expectedBucket" : 26 + }, + { + "key" : "42", + "seed" : "9223372036854775807", + "count" : 10, + "expectedBucket" : 5 + }, + { + "key" : "42", + "seed" : "9223372036854775807", + "count" : 50, + "expectedBucket" : 45 + }, + { + "key" : "42", + "seed" : "9223372036854775807", + "count" : 100, + "expectedBucket" : 45 + }, + { + "key" : "-7", + "seed" : "0", + "count" : 10, + "expectedBucket" : 4 + }, + { + "key" : "-7", + "seed" : "0", + "count" : 50, + "expectedBucket" : 14 + }, + { + "key" : "-7", + "seed" : "0", + "count" : 100, + "expectedBucket" : 14 + }, + { + "key" : "-7", + "seed" : "1", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "-7", + "seed" : "1", + "count" : 50, + "expectedBucket" : 27 + }, + { + "key" : "-7", + "seed" : "1", + "count" : 100, + "expectedBucket" : 77 + }, + { + "key" : "-7", + "seed" : "-1", + "count" : 10, + "expectedBucket" : 1 + }, + { + "key" : "-7", + "seed" : "-1", + "count" : 50, + "expectedBucket" : 31 + }, + { + "key" : "-7", + "seed" : "-1", + "count" : 100, + "expectedBucket" : 31 + }, + { + "key" : "-7", + "seed" : "42", + "count" : 10, + "expectedBucket" : 9 + }, + { + "key" : "-7", + "seed" : "42", + "count" : 50, + "expectedBucket" : 39 + }, + { + "key" : "-7", + "seed" : "42", + "count" : 100, + "expectedBucket" : 89 + }, + { + "key" : "-7", + "seed" : "-123456789012345", + "count" : 10, + "expectedBucket" : 8 + }, + { + "key" : "-7", + "seed" : "-123456789012345", + "count" : 50, + "expectedBucket" : 28 + }, + { + "key" : "-7", + "seed" : "-123456789012345", + "count" : 100, + "expectedBucket" : 78 + }, + { + "key" : "-7", + "seed" : "8589934591", + "count" : 10, + "expectedBucket" : 2 + }, + { + "key" : "-7", + "seed" : "8589934591", + "count" : 50, + "expectedBucket" : 22 + }, + { + "key" : "-7", + "seed" : "8589934591", + "count" : 100, + "expectedBucket" : 22 + }, + { + "key" : "-7", + "seed" : "-4294967295", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "-7", + "seed" : "-4294967295", + "count" : 50, + "expectedBucket" : 7 + }, + { + "key" : "-7", + "seed" : "-4294967295", + "count" : 100, + "expectedBucket" : 7 + }, + { + "key" : "-7", + "seed" : "-9223372036854775808", + "count" : 10, + "expectedBucket" : 1 + }, + { + "key" : "-7", + "seed" : "-9223372036854775808", + "count" : 50, + "expectedBucket" : 21 + }, + { + "key" : "-7", + "seed" : "-9223372036854775808", + "count" : 100, + "expectedBucket" : 71 + }, + { + "key" : "-7", + "seed" : "9223372036854775807", + "count" : 10, + "expectedBucket" : 3 + }, + { + "key" : "-7", + "seed" : "9223372036854775807", + "count" : 50, + "expectedBucket" : 23 + }, + { + "key" : "-7", + "seed" : "9223372036854775807", + "count" : 100, + "expectedBucket" : 73 + }, + { + "key" : "0", + "seed" : "0", + "count" : 10, + "expectedBucket" : 4 + }, + { + "key" : "0", + "seed" : "0", + "count" : 50, + "expectedBucket" : 44 + }, + { + "key" : "0", + "seed" : "0", + "count" : 100, + "expectedBucket" : 94 + }, + { + "key" : "0", + "seed" : "1", + "count" : 10, + "expectedBucket" : 2 + }, + { + "key" : "0", + "seed" : "1", + "count" : 50, + "expectedBucket" : 22 + }, + { + "key" : "0", + "seed" : "1", + "count" : 100, + "expectedBucket" : 72 + }, + { + "key" : "0", + "seed" : "-1", + "count" : 10, + "expectedBucket" : 6 + }, + { + "key" : "0", + "seed" : "-1", + "count" : 50, + "expectedBucket" : 6 + }, + { + "key" : "0", + "seed" : "-1", + "count" : 100, + "expectedBucket" : 56 + }, + { + "key" : "0", + "seed" : "42", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "0", + "seed" : "42", + "count" : 50, + "expectedBucket" : 27 + }, + { + "key" : "0", + "seed" : "42", + "count" : 100, + "expectedBucket" : 77 + }, + { + "key" : "0", + "seed" : "-123456789012345", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "0", + "seed" : "-123456789012345", + "count" : 50, + "expectedBucket" : 7 + }, + { + "key" : "0", + "seed" : "-123456789012345", + "count" : 100, + "expectedBucket" : 7 + }, + { + "key" : "0", + "seed" : "8589934591", + "count" : 10, + "expectedBucket" : 6 + }, + { + "key" : "0", + "seed" : "8589934591", + "count" : 50, + "expectedBucket" : 46 + }, + { + "key" : "0", + "seed" : "8589934591", + "count" : 100, + "expectedBucket" : 46 + }, + { + "key" : "0", + "seed" : "-4294967295", + "count" : 10, + "expectedBucket" : 4 + }, + { + "key" : "0", + "seed" : "-4294967295", + "count" : 50, + "expectedBucket" : 24 + }, + { + "key" : "0", + "seed" : "-4294967295", + "count" : 100, + "expectedBucket" : 24 + }, + { + "key" : "0", + "seed" : "-9223372036854775808", + "count" : 10, + "expectedBucket" : 8 + }, + { + "key" : "0", + "seed" : "-9223372036854775808", + "count" : 50, + "expectedBucket" : 18 + }, + { + "key" : "0", + "seed" : "-9223372036854775808", + "count" : 100, + "expectedBucket" : 18 + }, + { + "key" : "0", + "seed" : "9223372036854775807", + "count" : 10, + "expectedBucket" : 8 + }, + { + "key" : "0", + "seed" : "9223372036854775807", + "count" : 50, + "expectedBucket" : 18 + }, + { + "key" : "0", + "seed" : "9223372036854775807", + "count" : 100, + "expectedBucket" : 68 + }, + { + "key" : "9007199254740991", + "seed" : "0", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : "9007199254740991", + "seed" : "0", + "count" : 50, + "expectedBucket" : 0 + }, + { + "key" : "9007199254740991", + "seed" : "0", + "count" : 100, + "expectedBucket" : 50 + }, + { + "key" : "9007199254740991", + "seed" : "1", + "count" : 10, + "expectedBucket" : 2 + }, + { + "key" : "9007199254740991", + "seed" : "1", + "count" : 50, + "expectedBucket" : 12 + }, + { + "key" : "9007199254740991", + "seed" : "1", + "count" : 100, + "expectedBucket" : 12 + }, + { + "key" : "9007199254740991", + "seed" : "-1", + "count" : 10, + "expectedBucket" : 5 + }, + { + "key" : "9007199254740991", + "seed" : "-1", + "count" : 50, + "expectedBucket" : 5 + }, + { + "key" : "9007199254740991", + "seed" : "-1", + "count" : 100, + "expectedBucket" : 5 + }, + { + "key" : "9007199254740991", + "seed" : "42", + "count" : 10, + "expectedBucket" : 4 + }, + { + "key" : "9007199254740991", + "seed" : "42", + "count" : 50, + "expectedBucket" : 34 + }, + { + "key" : "9007199254740991", + "seed" : "42", + "count" : 100, + "expectedBucket" : 34 + }, + { + "key" : "9007199254740991", + "seed" : "-123456789012345", + "count" : 10, + "expectedBucket" : 4 + }, + { + "key" : "9007199254740991", + "seed" : "-123456789012345", + "count" : 50, + "expectedBucket" : 34 + }, + { + "key" : "9007199254740991", + "seed" : "-123456789012345", + "count" : 100, + "expectedBucket" : 34 + }, + { + "key" : "9007199254740991", + "seed" : "8589934591", + "count" : 10, + "expectedBucket" : 5 + }, + { + "key" : "9007199254740991", + "seed" : "8589934591", + "count" : 50, + "expectedBucket" : 35 + }, + { + "key" : "9007199254740991", + "seed" : "8589934591", + "count" : 100, + "expectedBucket" : 85 + }, + { + "key" : "9007199254740991", + "seed" : "-4294967295", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "9007199254740991", + "seed" : "-4294967295", + "count" : 50, + "expectedBucket" : 47 + }, + { + "key" : "9007199254740991", + "seed" : "-4294967295", + "count" : 100, + "expectedBucket" : 47 + }, + { + "key" : "9007199254740991", + "seed" : "-9223372036854775808", + "count" : 10, + "expectedBucket" : 5 + }, + { + "key" : "9007199254740991", + "seed" : "-9223372036854775808", + "count" : 50, + "expectedBucket" : 45 + }, + { + "key" : "9007199254740991", + "seed" : "-9223372036854775808", + "count" : 100, + "expectedBucket" : 95 + }, + { + "key" : "9007199254740991", + "seed" : "9223372036854775807", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : "9007199254740991", + "seed" : "9223372036854775807", + "count" : 50, + "expectedBucket" : 30 + }, + { + "key" : "9007199254740991", + "seed" : "9223372036854775807", + "count" : 100, + "expectedBucket" : 80 + }, + { + "key" : "-9007199254740991", + "seed" : "0", + "count" : 10, + "expectedBucket" : 3 + }, + { + "key" : "-9007199254740991", + "seed" : "0", + "count" : 50, + "expectedBucket" : 33 + }, + { + "key" : "-9007199254740991", + "seed" : "0", + "count" : 100, + "expectedBucket" : 83 + }, + { + "key" : "-9007199254740991", + "seed" : "1", + "count" : 10, + "expectedBucket" : 5 + }, + { + "key" : "-9007199254740991", + "seed" : "1", + "count" : 50, + "expectedBucket" : 35 + }, + { + "key" : "-9007199254740991", + "seed" : "1", + "count" : 100, + "expectedBucket" : 35 + }, + { + "key" : "-9007199254740991", + "seed" : "-1", + "count" : 10, + "expectedBucket" : 5 + }, + { + "key" : "-9007199254740991", + "seed" : "-1", + "count" : 50, + "expectedBucket" : 25 + }, + { + "key" : "-9007199254740991", + "seed" : "-1", + "count" : 100, + "expectedBucket" : 25 + }, + { + "key" : "-9007199254740991", + "seed" : "42", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "-9007199254740991", + "seed" : "42", + "count" : 50, + "expectedBucket" : 27 + }, + { + "key" : "-9007199254740991", + "seed" : "42", + "count" : 100, + "expectedBucket" : 77 + }, + { + "key" : "-9007199254740991", + "seed" : "-123456789012345", + "count" : 10, + "expectedBucket" : 6 + }, + { + "key" : "-9007199254740991", + "seed" : "-123456789012345", + "count" : 50, + "expectedBucket" : 36 + }, + { + "key" : "-9007199254740991", + "seed" : "-123456789012345", + "count" : 100, + "expectedBucket" : 86 + }, + { + "key" : "-9007199254740991", + "seed" : "8589934591", + "count" : 10, + "expectedBucket" : 8 + }, + { + "key" : "-9007199254740991", + "seed" : "8589934591", + "count" : 50, + "expectedBucket" : 48 + }, + { + "key" : "-9007199254740991", + "seed" : "8589934591", + "count" : 100, + "expectedBucket" : 48 + }, + { + "key" : "-9007199254740991", + "seed" : "-4294967295", + "count" : 10, + "expectedBucket" : 9 + }, + { + "key" : "-9007199254740991", + "seed" : "-4294967295", + "count" : 50, + "expectedBucket" : 39 + }, + { + "key" : "-9007199254740991", + "seed" : "-4294967295", + "count" : 100, + "expectedBucket" : 89 + }, + { + "key" : "-9007199254740991", + "seed" : "-9223372036854775808", + "count" : 10, + "expectedBucket" : 6 + }, + { + "key" : "-9007199254740991", + "seed" : "-9223372036854775808", + "count" : 50, + "expectedBucket" : 46 + }, + { + "key" : "-9007199254740991", + "seed" : "-9223372036854775808", + "count" : 100, + "expectedBucket" : 96 + }, + { + "key" : "-9007199254740991", + "seed" : "9223372036854775807", + "count" : 10, + "expectedBucket" : 2 + }, + { + "key" : "-9007199254740991", + "seed" : "9223372036854775807", + "count" : 50, + "expectedBucket" : 42 + }, + { + "key" : "-9007199254740991", + "seed" : "9223372036854775807", + "count" : 100, + "expectedBucket" : 92 + }, + { + "key" : "18446744073709551615", + "seed" : "0", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : "18446744073709551615", + "seed" : "0", + "count" : 50, + "expectedBucket" : 40 + }, + { + "key" : "18446744073709551615", + "seed" : "0", + "count" : 100, + "expectedBucket" : 90 + }, + { + "key" : "18446744073709551615", + "seed" : "1", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : "18446744073709551615", + "seed" : "1", + "count" : 50, + "expectedBucket" : 40 + }, + { + "key" : "18446744073709551615", + "seed" : "1", + "count" : 100, + "expectedBucket" : 90 + }, + { + "key" : "18446744073709551615", + "seed" : "-1", + "count" : 10, + "expectedBucket" : 2 + }, + { + "key" : "18446744073709551615", + "seed" : "-1", + "count" : 50, + "expectedBucket" : 42 + }, + { + "key" : "18446744073709551615", + "seed" : "-1", + "count" : 100, + "expectedBucket" : 42 + }, + { + "key" : "18446744073709551615", + "seed" : "42", + "count" : 10, + "expectedBucket" : 8 + }, + { + "key" : "18446744073709551615", + "seed" : "42", + "count" : 50, + "expectedBucket" : 8 + }, + { + "key" : "18446744073709551615", + "seed" : "42", + "count" : 100, + "expectedBucket" : 58 + }, + { + "key" : "18446744073709551615", + "seed" : "-123456789012345", + "count" : 10, + "expectedBucket" : 3 + }, + { + "key" : "18446744073709551615", + "seed" : "-123456789012345", + "count" : 50, + "expectedBucket" : 43 + }, + { + "key" : "18446744073709551615", + "seed" : "-123456789012345", + "count" : 100, + "expectedBucket" : 93 + }, + { + "key" : "18446744073709551615", + "seed" : "8589934591", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "18446744073709551615", + "seed" : "8589934591", + "count" : 50, + "expectedBucket" : 37 + }, + { + "key" : "18446744073709551615", + "seed" : "8589934591", + "count" : 100, + "expectedBucket" : 37 + }, + { + "key" : "18446744073709551615", + "seed" : "-4294967295", + "count" : 10, + "expectedBucket" : 1 + }, + { + "key" : "18446744073709551615", + "seed" : "-4294967295", + "count" : 50, + "expectedBucket" : 31 + }, + { + "key" : "18446744073709551615", + "seed" : "-4294967295", + "count" : 100, + "expectedBucket" : 31 + }, + { + "key" : "18446744073709551615", + "seed" : "-9223372036854775808", + "count" : 10, + "expectedBucket" : 1 + }, + { + "key" : "18446744073709551615", + "seed" : "-9223372036854775808", + "count" : 50, + "expectedBucket" : 1 + }, + { + "key" : "18446744073709551615", + "seed" : "-9223372036854775808", + "count" : 100, + "expectedBucket" : 1 + }, + { + "key" : "18446744073709551615", + "seed" : "9223372036854775807", + "count" : 10, + "expectedBucket" : 2 + }, + { + "key" : "18446744073709551615", + "seed" : "9223372036854775807", + "count" : 50, + "expectedBucket" : 42 + }, + { + "key" : "18446744073709551615", + "seed" : "9223372036854775807", + "count" : 100, + "expectedBucket" : 42 + }, + { + "key" : "a b", + "seed" : "0", + "count" : 10, + "expectedBucket" : 5 + }, + { + "key" : "a b", + "seed" : "0", + "count" : 50, + "expectedBucket" : 25 + }, + { + "key" : "a b", + "seed" : "0", + "count" : 100, + "expectedBucket" : 75 + }, + { + "key" : "a b", + "seed" : "1", + "count" : 10, + "expectedBucket" : 6 + }, + { + "key" : "a b", + "seed" : "1", + "count" : 50, + "expectedBucket" : 46 + }, + { + "key" : "a b", + "seed" : "1", + "count" : 100, + "expectedBucket" : 46 + }, + { + "key" : "a b", + "seed" : "-1", + "count" : 10, + "expectedBucket" : 1 + }, + { + "key" : "a b", + "seed" : "-1", + "count" : 50, + "expectedBucket" : 21 + }, + { + "key" : "a b", + "seed" : "-1", + "count" : 100, + "expectedBucket" : 71 + }, + { + "key" : "a b", + "seed" : "42", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : "a b", + "seed" : "42", + "count" : 50, + "expectedBucket" : 30 + }, + { + "key" : "a b", + "seed" : "42", + "count" : 100, + "expectedBucket" : 30 + }, + { + "key" : "a b", + "seed" : "-123456789012345", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "a b", + "seed" : "-123456789012345", + "count" : 50, + "expectedBucket" : 27 + }, + { + "key" : "a b", + "seed" : "-123456789012345", + "count" : 100, + "expectedBucket" : 27 + }, + { + "key" : "a b", + "seed" : "8589934591", + "count" : 10, + "expectedBucket" : 4 + }, + { + "key" : "a b", + "seed" : "8589934591", + "count" : 50, + "expectedBucket" : 4 + }, + { + "key" : "a b", + "seed" : "8589934591", + "count" : 100, + "expectedBucket" : 4 + }, + { + "key" : "a b", + "seed" : "-4294967295", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "a b", + "seed" : "-4294967295", + "count" : 50, + "expectedBucket" : 7 + }, + { + "key" : "a b", + "seed" : "-4294967295", + "count" : 100, + "expectedBucket" : 7 + }, + { + "key" : "a b", + "seed" : "-9223372036854775808", + "count" : 10, + "expectedBucket" : 9 + }, + { + "key" : "a b", + "seed" : "-9223372036854775808", + "count" : 50, + "expectedBucket" : 9 + }, + { + "key" : "a b", + "seed" : "-9223372036854775808", + "count" : 100, + "expectedBucket" : 59 + }, + { + "key" : "a b", + "seed" : "9223372036854775807", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : "a b", + "seed" : "9223372036854775807", + "count" : 50, + "expectedBucket" : 10 + }, + { + "key" : "a b", + "seed" : "9223372036854775807", + "count" : 100, + "expectedBucket" : 60 + }, + { + "key" : " lead", + "seed" : "0", + "count" : 10, + "expectedBucket" : 6 + }, + { + "key" : " lead", + "seed" : "0", + "count" : 50, + "expectedBucket" : 36 + }, + { + "key" : " lead", + "seed" : "0", + "count" : 100, + "expectedBucket" : 36 + }, + { + "key" : " lead", + "seed" : "1", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : " lead", + "seed" : "1", + "count" : 50, + "expectedBucket" : 30 + }, + { + "key" : " lead", + "seed" : "1", + "count" : 100, + "expectedBucket" : 30 + }, + { + "key" : " lead", + "seed" : "-1", + "count" : 10, + "expectedBucket" : 5 + }, + { + "key" : " lead", + "seed" : "-1", + "count" : 50, + "expectedBucket" : 35 + }, + { + "key" : " lead", + "seed" : "-1", + "count" : 100, + "expectedBucket" : 85 + }, + { + "key" : " lead", + "seed" : "42", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : " lead", + "seed" : "42", + "count" : 50, + "expectedBucket" : 7 + }, + { + "key" : " lead", + "seed" : "42", + "count" : 100, + "expectedBucket" : 57 + }, + { + "key" : " lead", + "seed" : "-123456789012345", + "count" : 10, + "expectedBucket" : 9 + }, + { + "key" : " lead", + "seed" : "-123456789012345", + "count" : 50, + "expectedBucket" : 29 + }, + { + "key" : " lead", + "seed" : "-123456789012345", + "count" : 100, + "expectedBucket" : 29 + }, + { + "key" : " lead", + "seed" : "8589934591", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : " lead", + "seed" : "8589934591", + "count" : 50, + "expectedBucket" : 10 + }, + { + "key" : " lead", + "seed" : "8589934591", + "count" : 100, + "expectedBucket" : 10 + }, + { + "key" : " lead", + "seed" : "-4294967295", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : " lead", + "seed" : "-4294967295", + "count" : 50, + "expectedBucket" : 37 + }, + { + "key" : " lead", + "seed" : "-4294967295", + "count" : 100, + "expectedBucket" : 87 + }, + { + "key" : " lead", + "seed" : "-9223372036854775808", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : " lead", + "seed" : "-9223372036854775808", + "count" : 50, + "expectedBucket" : 7 + }, + { + "key" : " lead", + "seed" : "-9223372036854775808", + "count" : 100, + "expectedBucket" : 57 + }, + { + "key" : " lead", + "seed" : "9223372036854775807", + "count" : 10, + "expectedBucket" : 6 + }, + { + "key" : " lead", + "seed" : "9223372036854775807", + "count" : 50, + "expectedBucket" : 6 + }, + { + "key" : " lead", + "seed" : "9223372036854775807", + "count" : 100, + "expectedBucket" : 6 + }, + { + "key" : "x'y\\z", + "seed" : "0", + "count" : 10, + "expectedBucket" : 9 + }, + { + "key" : "x'y\\z", + "seed" : "0", + "count" : 50, + "expectedBucket" : 29 + }, + { + "key" : "x'y\\z", + "seed" : "0", + "count" : 100, + "expectedBucket" : 79 + }, + { + "key" : "x'y\\z", + "seed" : "1", + "count" : 10, + "expectedBucket" : 8 + }, + { + "key" : "x'y\\z", + "seed" : "1", + "count" : 50, + "expectedBucket" : 48 + }, + { + "key" : "x'y\\z", + "seed" : "1", + "count" : 100, + "expectedBucket" : 98 + }, + { + "key" : "x'y\\z", + "seed" : "-1", + "count" : 10, + "expectedBucket" : 8 + }, + { + "key" : "x'y\\z", + "seed" : "-1", + "count" : 50, + "expectedBucket" : 8 + }, + { + "key" : "x'y\\z", + "seed" : "-1", + "count" : 100, + "expectedBucket" : 58 + }, + { + "key" : "x'y\\z", + "seed" : "42", + "count" : 10, + "expectedBucket" : 5 + }, + { + "key" : "x'y\\z", + "seed" : "42", + "count" : 50, + "expectedBucket" : 45 + }, + { + "key" : "x'y\\z", + "seed" : "42", + "count" : 100, + "expectedBucket" : 95 + }, + { + "key" : "x'y\\z", + "seed" : "-123456789012345", + "count" : 10, + "expectedBucket" : 5 + }, + { + "key" : "x'y\\z", + "seed" : "-123456789012345", + "count" : 50, + "expectedBucket" : 5 + }, + { + "key" : "x'y\\z", + "seed" : "-123456789012345", + "count" : 100, + "expectedBucket" : 55 + }, + { + "key" : "x'y\\z", + "seed" : "8589934591", + "count" : 10, + "expectedBucket" : 0 + }, + { + "key" : "x'y\\z", + "seed" : "8589934591", + "count" : 50, + "expectedBucket" : 40 + }, + { + "key" : "x'y\\z", + "seed" : "8589934591", + "count" : 100, + "expectedBucket" : 40 + }, + { + "key" : "x'y\\z", + "seed" : "-4294967295", + "count" : 10, + "expectedBucket" : 6 + }, + { + "key" : "x'y\\z", + "seed" : "-4294967295", + "count" : 50, + "expectedBucket" : 36 + }, + { + "key" : "x'y\\z", + "seed" : "-4294967295", + "count" : 100, + "expectedBucket" : 36 + }, + { + "key" : "x'y\\z", + "seed" : "-9223372036854775808", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "x'y\\z", + "seed" : "-9223372036854775808", + "count" : 50, + "expectedBucket" : 37 + }, + { + "key" : "x'y\\z", + "seed" : "-9223372036854775808", + "count" : 100, + "expectedBucket" : 87 + }, + { + "key" : "x'y\\z", + "seed" : "9223372036854775807", + "count" : 10, + "expectedBucket" : 7 + }, + { + "key" : "x'y\\z", + "seed" : "9223372036854775807", + "count" : 50, + "expectedBucket" : 7 + }, + { + "key" : "x'y\\z", + "seed" : "9223372036854775807", + "count" : 100, + "expectedBucket" : 57 + }, + { + "key" : "a\u001Fb", + "seed" : "0", + "count" : 10, + "expectedBucket" : 2 + }, + { + "key" : "a\u001Fb", + "seed" : "0", + "count" : 50, + "expectedBucket" : 12 + }, + { + "key" : "a\u001Fb", + "seed" : "0", + "count" : 100, + "expectedBucket" : 12 + }, + { + "key" : "a\u001Fb", + "seed" : "1", + "count" : 10, + "expectedBucket" : 1 + }, + { + "key" : "a\u001Fb", + "seed" : "1", + "count" : 50, + "expectedBucket" : 21 + }, + { + "key" : "a\u001Fb", + "seed" : "1", + "count" : 100, + "expectedBucket" : 71 + }, + { + "key" : "a\u001Fb", + "seed" : "-1", + "count" : 10, + "expectedBucket" : 4 + }, + { + "key" : "a\u001Fb", + "seed" : "-1", + "count" : 50, + "expectedBucket" : 14 + }, + { + "key" : "a\u001Fb", + "seed" : "-1", + "count" : 100, + "expectedBucket" : 64 + }, + { + "key" : "a\u001Fb", + "seed" : "42", + "count" : 10, + "expectedBucket" : 6 + }, + { + "key" : "a\u001Fb", + "seed" : "42", + "count" : 50, + "expectedBucket" : 46 + }, + { + "key" : "a\u001Fb", + "seed" : "42", + "count" : 100, + "expectedBucket" : 96 + }, + { + "key" : "a\u001Fb", + "seed" : "-123456789012345", + "count" : 10, + "expectedBucket" : 4 + }, + { + "key" : "a\u001Fb", + "seed" : "-123456789012345", + "count" : 50, + "expectedBucket" : 34 + }, + { + "key" : "a\u001Fb", + "seed" : "-123456789012345", + "count" : 100, + "expectedBucket" : 34 + }, + { + "key" : "a\u001Fb", + "seed" : "8589934591", + "count" : 10, + "expectedBucket" : 9 + }, + { + "key" : "a\u001Fb", + "seed" : "8589934591", + "count" : 50, + "expectedBucket" : 29 + }, + { + "key" : "a\u001Fb", + "seed" : "8589934591", + "count" : 100, + "expectedBucket" : 29 + }, + { + "key" : "a\u001Fb", + "seed" : "-4294967295", + "count" : 10, + "expectedBucket" : 4 + }, + { + "key" : "a\u001Fb", + "seed" : "-4294967295", + "count" : 50, + "expectedBucket" : 24 + }, + { + "key" : "a\u001Fb", + "seed" : "-4294967295", + "count" : 100, + "expectedBucket" : 74 + }, + { + "key" : "a\u001Fb", + "seed" : "-9223372036854775808", + "count" : 10, + "expectedBucket" : 3 + }, + { + "key" : "a\u001Fb", + "seed" : "-9223372036854775808", + "count" : 50, + "expectedBucket" : 43 + }, + { + "key" : "a\u001Fb", + "seed" : "-9223372036854775808", + "count" : 100, + "expectedBucket" : 43 + }, + { + "key" : "a\u001Fb", + "seed" : "9223372036854775807", + "count" : 10, + "expectedBucket" : 9 + }, + { + "key" : "a\u001Fb", + "seed" : "9223372036854775807", + "count" : 50, + "expectedBucket" : 9 + }, + { + "key" : "a\u001Fb", + "seed" : "9223372036854775807", + "count" : 100, + "expectedBucket" : 59 + } + ] +} diff --git a/core-api/src/test/resources/switchback/golden-vectors.json b/core-api/src/test/resources/switchback/golden-vectors.json new file mode 100644 index 0000000..b314abe --- /dev/null +++ b/core-api/src/test/resources/switchback/golden-vectors.json @@ -0,0 +1,1392 @@ +{ + "assignSwitchback": [ + {"clusterUnit": "market_1", "periodIndex": "0", "seed": "0", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "1", "seed": "0", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "2", "seed": "0", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "5", "seed": "0", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "9", "seed": "0", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "10", "seed": "0", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "11", "seed": "0", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "23", "seed": "0", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "100", "seed": "0", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "-1", "seed": "0", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "-2", "seed": "0", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-11", "seed": "0", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "-9223372036854775808", "seed": "0", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "9223372036854775807", "seed": "0", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "0", "seed": "1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "1", "seed": "1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "2", "seed": "1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "5", "seed": "1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "9", "seed": "1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "10", "seed": "1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "11", "seed": "1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "23", "seed": "1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "100", "seed": "1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-1", "seed": "1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-2", "seed": "1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "-11", "seed": "1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-9223372036854775808", "seed": "1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "9223372036854775807", "seed": "1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "0", "seed": "42", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "1", "seed": "42", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "2", "seed": "42", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "5", "seed": "42", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "9", "seed": "42", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "10", "seed": "42", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "11", "seed": "42", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "23", "seed": "42", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "100", "seed": "42", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "-1", "seed": "42", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-2", "seed": "42", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "-11", "seed": "42", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "-9223372036854775808", "seed": "42", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "9223372036854775807", "seed": "42", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "0", "seed": "-1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "1", "seed": "-1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "2", "seed": "-1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "5", "seed": "-1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "9", "seed": "-1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "10", "seed": "-1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "11", "seed": "-1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "23", "seed": "-1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "100", "seed": "-1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-1", "seed": "-1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-2", "seed": "-1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "-11", "seed": "-1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-9223372036854775808", "seed": "-1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "9223372036854775807", "seed": "-1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "0", "seed": "123456789", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "1", "seed": "123456789", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "2", "seed": "123456789", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "5", "seed": "123456789", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "9", "seed": "123456789", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "10", "seed": "123456789", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "11", "seed": "123456789", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "23", "seed": "123456789", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "100", "seed": "123456789", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-1", "seed": "123456789", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-2", "seed": "123456789", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "-11", "seed": "123456789", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-9223372036854775808", "seed": "123456789", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "9223372036854775807", "seed": "123456789", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "0", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "1", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "2", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "5", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "9", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "10", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "11", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "23", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "100", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-1", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-2", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "-11", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-9223372036854775808", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "9223372036854775807", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "0", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "1", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "2", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "5", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "9", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "10", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "11", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "23", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "100", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-1", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "-2", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-11", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-9223372036854775808", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "9223372036854775807", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "0", "seed": "0", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "1", "seed": "0", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "2", "seed": "0", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "5", "seed": "0", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "9", "seed": "0", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "10", "seed": "0", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "11", "seed": "0", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "23", "seed": "0", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "100", "seed": "0", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "-1", "seed": "0", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "-2", "seed": "0", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "-11", "seed": "0", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-9223372036854775808", "seed": "0", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "9223372036854775807", "seed": "0", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "0", "seed": "1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "1", "seed": "1", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "2", "seed": "1", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "5", "seed": "1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "9", "seed": "1", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "10", "seed": "1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "11", "seed": "1", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "23", "seed": "1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "100", "seed": "1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "-1", "seed": "1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "-2", "seed": "1", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-11", "seed": "1", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "-9223372036854775808", "seed": "1", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "9223372036854775807", "seed": "1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "0", "seed": "42", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "1", "seed": "42", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "2", "seed": "42", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "5", "seed": "42", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "9", "seed": "42", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "10", "seed": "42", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "11", "seed": "42", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "23", "seed": "42", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "100", "seed": "42", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-1", "seed": "42", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-2", "seed": "42", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "-11", "seed": "42", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-9223372036854775808", "seed": "42", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "9223372036854775807", "seed": "42", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "0", "seed": "-1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "1", "seed": "-1", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "2", "seed": "-1", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "5", "seed": "-1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "9", "seed": "-1", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "10", "seed": "-1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "11", "seed": "-1", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "23", "seed": "-1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "100", "seed": "-1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "-1", "seed": "-1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "-2", "seed": "-1", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-11", "seed": "-1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "-9223372036854775808", "seed": "-1", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "9223372036854775807", "seed": "-1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "0", "seed": "123456789", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "1", "seed": "123456789", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "2", "seed": "123456789", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "5", "seed": "123456789", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "9", "seed": "123456789", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "10", "seed": "123456789", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "11", "seed": "123456789", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "23", "seed": "123456789", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "100", "seed": "123456789", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-1", "seed": "123456789", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "-2", "seed": "123456789", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-11", "seed": "123456789", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-9223372036854775808", "seed": "123456789", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "9223372036854775807", "seed": "123456789", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "0", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "1", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "2", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "5", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "9", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "10", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "11", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "23", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "100", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-1", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "-2", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-11", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "-9223372036854775808", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "9223372036854775807", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "0", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "1", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "2", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "5", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "9", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "10", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "11", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "23", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "100", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-1", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "-2", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "-11", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-9223372036854775808", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "9223372036854775807", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "0", "seed": "0", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "1", "seed": "0", "armCount": 10, "expectedVariant": 3}, + {"clusterUnit": "market_1", "periodIndex": "2", "seed": "0", "armCount": 10, "expectedVariant": 7}, + {"clusterUnit": "market_1", "periodIndex": "5", "seed": "0", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "9", "seed": "0", "armCount": 10, "expectedVariant": 9}, + {"clusterUnit": "market_1", "periodIndex": "10", "seed": "0", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": "market_1", "periodIndex": "11", "seed": "0", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "23", "seed": "0", "armCount": 10, "expectedVariant": 8}, + {"clusterUnit": "market_1", "periodIndex": "100", "seed": "0", "armCount": 10, "expectedVariant": 9}, + {"clusterUnit": "market_1", "periodIndex": "-1", "seed": "0", "armCount": 10, "expectedVariant": 8}, + {"clusterUnit": "market_1", "periodIndex": "-2", "seed": "0", "armCount": 10, "expectedVariant": 3}, + {"clusterUnit": "market_1", "periodIndex": "-11", "seed": "0", "armCount": 10, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "-9223372036854775808", "seed": "0", "armCount": 10, "expectedVariant": 9}, + {"clusterUnit": "market_1", "periodIndex": "9223372036854775807", "seed": "0", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "0", "seed": "1", "armCount": 10, "expectedVariant": 9}, + {"clusterUnit": "market_1", "periodIndex": "1", "seed": "1", "armCount": 10, "expectedVariant": 4}, + {"clusterUnit": "market_1", "periodIndex": "2", "seed": "1", "armCount": 10, "expectedVariant": 3}, + {"clusterUnit": "market_1", "periodIndex": "5", "seed": "1", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "9", "seed": "1", "armCount": 10, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "10", "seed": "1", "armCount": 10, "expectedVariant": 3}, + {"clusterUnit": "market_1", "periodIndex": "11", "seed": "1", "armCount": 10, "expectedVariant": 8}, + {"clusterUnit": "market_1", "periodIndex": "23", "seed": "1", "armCount": 10, "expectedVariant": 3}, + {"clusterUnit": "market_1", "periodIndex": "100", "seed": "1", "armCount": 10, "expectedVariant": 5}, + {"clusterUnit": "market_1", "periodIndex": "-1", "seed": "1", "armCount": 10, "expectedVariant": 3}, + {"clusterUnit": "market_1", "periodIndex": "-2", "seed": "1", "armCount": 10, "expectedVariant": 4}, + {"clusterUnit": "market_1", "periodIndex": "-11", "seed": "1", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-9223372036854775808", "seed": "1", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": "market_1", "periodIndex": "9223372036854775807", "seed": "1", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "0", "seed": "42", "armCount": 10, "expectedVariant": 3}, + {"clusterUnit": "market_1", "periodIndex": "1", "seed": "42", "armCount": 10, "expectedVariant": 7}, + {"clusterUnit": "market_1", "periodIndex": "2", "seed": "42", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "5", "seed": "42", "armCount": 10, "expectedVariant": 8}, + {"clusterUnit": "market_1", "periodIndex": "9", "seed": "42", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": "market_1", "periodIndex": "10", "seed": "42", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "11", "seed": "42", "armCount": 10, "expectedVariant": 8}, + {"clusterUnit": "market_1", "periodIndex": "23", "seed": "42", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "100", "seed": "42", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "-1", "seed": "42", "armCount": 10, "expectedVariant": 9}, + {"clusterUnit": "market_1", "periodIndex": "-2", "seed": "42", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "-11", "seed": "42", "armCount": 10, "expectedVariant": 7}, + {"clusterUnit": "market_1", "periodIndex": "-9223372036854775808", "seed": "42", "armCount": 10, "expectedVariant": 3}, + {"clusterUnit": "market_1", "periodIndex": "9223372036854775807", "seed": "42", "armCount": 10, "expectedVariant": 5}, + {"clusterUnit": "market_1", "periodIndex": "0", "seed": "-1", "armCount": 10, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "1", "seed": "-1", "armCount": 10, "expectedVariant": 5}, + {"clusterUnit": "market_1", "periodIndex": "2", "seed": "-1", "armCount": 10, "expectedVariant": 7}, + {"clusterUnit": "market_1", "periodIndex": "5", "seed": "-1", "armCount": 10, "expectedVariant": 4}, + {"clusterUnit": "market_1", "periodIndex": "9", "seed": "-1", "armCount": 10, "expectedVariant": 8}, + {"clusterUnit": "market_1", "periodIndex": "10", "seed": "-1", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": "market_1", "periodIndex": "11", "seed": "-1", "armCount": 10, "expectedVariant": 4}, + {"clusterUnit": "market_1", "periodIndex": "23", "seed": "-1", "armCount": 10, "expectedVariant": 8}, + {"clusterUnit": "market_1", "periodIndex": "100", "seed": "-1", "armCount": 10, "expectedVariant": 3}, + {"clusterUnit": "market_1", "periodIndex": "-1", "seed": "-1", "armCount": 10, "expectedVariant": 3}, + {"clusterUnit": "market_1", "periodIndex": "-2", "seed": "-1", "armCount": 10, "expectedVariant": 7}, + {"clusterUnit": "market_1", "periodIndex": "-11", "seed": "-1", "armCount": 10, "expectedVariant": 9}, + {"clusterUnit": "market_1", "periodIndex": "-9223372036854775808", "seed": "-1", "armCount": 10, "expectedVariant": 3}, + {"clusterUnit": "market_1", "periodIndex": "9223372036854775807", "seed": "-1", "armCount": 10, "expectedVariant": 7}, + {"clusterUnit": "market_1", "periodIndex": "0", "seed": "123456789", "armCount": 10, "expectedVariant": 3}, + {"clusterUnit": "market_1", "periodIndex": "1", "seed": "123456789", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": "market_1", "periodIndex": "2", "seed": "123456789", "armCount": 10, "expectedVariant": 5}, + {"clusterUnit": "market_1", "periodIndex": "5", "seed": "123456789", "armCount": 10, "expectedVariant": 9}, + {"clusterUnit": "market_1", "periodIndex": "9", "seed": "123456789", "armCount": 10, "expectedVariant": 7}, + {"clusterUnit": "market_1", "periodIndex": "10", "seed": "123456789", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": "market_1", "periodIndex": "11", "seed": "123456789", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "23", "seed": "123456789", "armCount": 10, "expectedVariant": 5}, + {"clusterUnit": "market_1", "periodIndex": "100", "seed": "123456789", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "-1", "seed": "123456789", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "-2", "seed": "123456789", "armCount": 10, "expectedVariant": 4}, + {"clusterUnit": "market_1", "periodIndex": "-11", "seed": "123456789", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": "market_1", "periodIndex": "-9223372036854775808", "seed": "123456789", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "9223372036854775807", "seed": "123456789", "armCount": 10, "expectedVariant": 3}, + {"clusterUnit": "market_1", "periodIndex": "0", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 3}, + {"clusterUnit": "market_1", "periodIndex": "1", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 9}, + {"clusterUnit": "market_1", "periodIndex": "2", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": "market_1", "periodIndex": "5", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 7}, + {"clusterUnit": "market_1", "periodIndex": "9", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "10", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 3}, + {"clusterUnit": "market_1", "periodIndex": "11", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "23", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 3}, + {"clusterUnit": "market_1", "periodIndex": "100", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": "market_1", "periodIndex": "-1", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "-2", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "-11", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 5}, + {"clusterUnit": "market_1", "periodIndex": "-9223372036854775808", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 8}, + {"clusterUnit": "market_1", "periodIndex": "9223372036854775807", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": "market_1", "periodIndex": "0", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 8}, + {"clusterUnit": "market_1", "periodIndex": "1", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "2", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": "market_1", "periodIndex": "5", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 7}, + {"clusterUnit": "market_1", "periodIndex": "9", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 9}, + {"clusterUnit": "market_1", "periodIndex": "10", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "11", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 5}, + {"clusterUnit": "market_1", "periodIndex": "23", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 3}, + {"clusterUnit": "market_1", "periodIndex": "100", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 7}, + {"clusterUnit": "market_1", "periodIndex": "-1", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 8}, + {"clusterUnit": "market_1", "periodIndex": "-2", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 1}, + {"clusterUnit": "market_1", "periodIndex": "-11", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": "market_1", "periodIndex": "-9223372036854775808", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": "market_1", "periodIndex": "9223372036854775807", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "0", "seed": "0", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "1", "seed": "0", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "2", "seed": "0", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "5", "seed": "0", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "9", "seed": "0", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "10", "seed": "0", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "11", "seed": "0", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "23", "seed": "0", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "100", "seed": "0", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "-1", "seed": "0", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "-2", "seed": "0", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "-11", "seed": "0", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "-9223372036854775808", "seed": "0", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "9223372036854775807", "seed": "0", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "0", "seed": "1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "1", "seed": "1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "2", "seed": "1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "5", "seed": "1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "9", "seed": "1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "10", "seed": "1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "11", "seed": "1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "23", "seed": "1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "100", "seed": "1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "-1", "seed": "1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "-2", "seed": "1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "-11", "seed": "1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "-9223372036854775808", "seed": "1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "9223372036854775807", "seed": "1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "0", "seed": "42", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "1", "seed": "42", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "2", "seed": "42", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "5", "seed": "42", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "9", "seed": "42", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "10", "seed": "42", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "11", "seed": "42", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "23", "seed": "42", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "100", "seed": "42", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "-1", "seed": "42", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "-2", "seed": "42", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "-11", "seed": "42", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "-9223372036854775808", "seed": "42", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "9223372036854775807", "seed": "42", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "0", "seed": "-1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "1", "seed": "-1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "2", "seed": "-1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "5", "seed": "-1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "9", "seed": "-1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "10", "seed": "-1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "11", "seed": "-1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "23", "seed": "-1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "100", "seed": "-1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "-1", "seed": "-1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "-2", "seed": "-1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "-11", "seed": "-1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "-9223372036854775808", "seed": "-1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "9223372036854775807", "seed": "-1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "0", "seed": "123456789", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "1", "seed": "123456789", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "2", "seed": "123456789", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "5", "seed": "123456789", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "9", "seed": "123456789", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "10", "seed": "123456789", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "11", "seed": "123456789", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "23", "seed": "123456789", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "100", "seed": "123456789", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "-1", "seed": "123456789", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "-2", "seed": "123456789", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "-11", "seed": "123456789", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "-9223372036854775808", "seed": "123456789", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "9223372036854775807", "seed": "123456789", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "0", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "1", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "2", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "5", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "9", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "10", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "11", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "23", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "100", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "-1", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "-2", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "-11", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "-9223372036854775808", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "9223372036854775807", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "0", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "1", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "2", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "5", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "9", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "10", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "11", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "23", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "100", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "-1", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "-2", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "-11", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "-9223372036854775808", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "9223372036854775807", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "0", "seed": "0", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "1", "seed": "0", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "2", "seed": "0", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "5", "seed": "0", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "9", "seed": "0", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "10", "seed": "0", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "11", "seed": "0", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "23", "seed": "0", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "100", "seed": "0", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "-1", "seed": "0", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "-2", "seed": "0", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "-11", "seed": "0", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "-9223372036854775808", "seed": "0", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "9223372036854775807", "seed": "0", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "0", "seed": "1", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "1", "seed": "1", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "2", "seed": "1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "5", "seed": "1", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "9", "seed": "1", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "10", "seed": "1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "11", "seed": "1", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "23", "seed": "1", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "100", "seed": "1", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "-1", "seed": "1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "-2", "seed": "1", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "-11", "seed": "1", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "-9223372036854775808", "seed": "1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "9223372036854775807", "seed": "1", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "0", "seed": "42", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "1", "seed": "42", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "2", "seed": "42", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "5", "seed": "42", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "9", "seed": "42", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "10", "seed": "42", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "11", "seed": "42", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "23", "seed": "42", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "100", "seed": "42", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "-1", "seed": "42", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "-2", "seed": "42", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "-11", "seed": "42", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "-9223372036854775808", "seed": "42", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "9223372036854775807", "seed": "42", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "0", "seed": "-1", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "1", "seed": "-1", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "2", "seed": "-1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "5", "seed": "-1", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "9", "seed": "-1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "10", "seed": "-1", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "11", "seed": "-1", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "23", "seed": "-1", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "100", "seed": "-1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "-1", "seed": "-1", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "-2", "seed": "-1", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "-11", "seed": "-1", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "-9223372036854775808", "seed": "-1", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "9223372036854775807", "seed": "-1", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "0", "seed": "123456789", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "1", "seed": "123456789", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "2", "seed": "123456789", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "5", "seed": "123456789", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "9", "seed": "123456789", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "10", "seed": "123456789", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "11", "seed": "123456789", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "23", "seed": "123456789", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "100", "seed": "123456789", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "-1", "seed": "123456789", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "-2", "seed": "123456789", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "-11", "seed": "123456789", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "-9223372036854775808", "seed": "123456789", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "9223372036854775807", "seed": "123456789", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "0", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "1", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "2", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "5", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "9", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "10", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "11", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "23", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "100", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "-1", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "-2", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "-11", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "-9223372036854775808", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "9223372036854775807", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "0", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "1", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "2", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "5", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "9", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "10", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "11", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "23", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "100", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "-1", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "-2", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "-11", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "-9223372036854775808", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "9223372036854775807", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "0", "seed": "0", "armCount": 10, "expectedVariant": 4}, + {"clusterUnit": "market_2", "periodIndex": "1", "seed": "0", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": "market_2", "periodIndex": "2", "seed": "0", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "5", "seed": "0", "armCount": 10, "expectedVariant": 8}, + {"clusterUnit": "market_2", "periodIndex": "9", "seed": "0", "armCount": 10, "expectedVariant": 3}, + {"clusterUnit": "market_2", "periodIndex": "10", "seed": "0", "armCount": 10, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "11", "seed": "0", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "23", "seed": "0", "armCount": 10, "expectedVariant": 3}, + {"clusterUnit": "market_2", "periodIndex": "100", "seed": "0", "armCount": 10, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "-1", "seed": "0", "armCount": 10, "expectedVariant": 8}, + {"clusterUnit": "market_2", "periodIndex": "-2", "seed": "0", "armCount": 10, "expectedVariant": 9}, + {"clusterUnit": "market_2", "periodIndex": "-11", "seed": "0", "armCount": 10, "expectedVariant": 5}, + {"clusterUnit": "market_2", "periodIndex": "-9223372036854775808", "seed": "0", "armCount": 10, "expectedVariant": 7}, + {"clusterUnit": "market_2", "periodIndex": "9223372036854775807", "seed": "0", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "0", "seed": "1", "armCount": 10, "expectedVariant": 4}, + {"clusterUnit": "market_2", "periodIndex": "1", "seed": "1", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "2", "seed": "1", "armCount": 10, "expectedVariant": 5}, + {"clusterUnit": "market_2", "periodIndex": "5", "seed": "1", "armCount": 10, "expectedVariant": 7}, + {"clusterUnit": "market_2", "periodIndex": "9", "seed": "1", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": "market_2", "periodIndex": "10", "seed": "1", "armCount": 10, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "11", "seed": "1", "armCount": 10, "expectedVariant": 7}, + {"clusterUnit": "market_2", "periodIndex": "23", "seed": "1", "armCount": 10, "expectedVariant": 5}, + {"clusterUnit": "market_2", "periodIndex": "100", "seed": "1", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": "market_2", "periodIndex": "-1", "seed": "1", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "-2", "seed": "1", "armCount": 10, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "-11", "seed": "1", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "-9223372036854775808", "seed": "1", "armCount": 10, "expectedVariant": 3}, + {"clusterUnit": "market_2", "periodIndex": "9223372036854775807", "seed": "1", "armCount": 10, "expectedVariant": 4}, + {"clusterUnit": "market_2", "periodIndex": "0", "seed": "42", "armCount": 10, "expectedVariant": 9}, + {"clusterUnit": "market_2", "periodIndex": "1", "seed": "42", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "2", "seed": "42", "armCount": 10, "expectedVariant": 3}, + {"clusterUnit": "market_2", "periodIndex": "5", "seed": "42", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": "market_2", "periodIndex": "9", "seed": "42", "armCount": 10, "expectedVariant": 4}, + {"clusterUnit": "market_2", "periodIndex": "10", "seed": "42", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "11", "seed": "42", "armCount": 10, "expectedVariant": 9}, + {"clusterUnit": "market_2", "periodIndex": "23", "seed": "42", "armCount": 10, "expectedVariant": 8}, + {"clusterUnit": "market_2", "periodIndex": "100", "seed": "42", "armCount": 10, "expectedVariant": 5}, + {"clusterUnit": "market_2", "periodIndex": "-1", "seed": "42", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "-2", "seed": "42", "armCount": 10, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "-11", "seed": "42", "armCount": 10, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "-9223372036854775808", "seed": "42", "armCount": 10, "expectedVariant": 5}, + {"clusterUnit": "market_2", "periodIndex": "9223372036854775807", "seed": "42", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "0", "seed": "-1", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "1", "seed": "-1", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": "market_2", "periodIndex": "2", "seed": "-1", "armCount": 10, "expectedVariant": 9}, + {"clusterUnit": "market_2", "periodIndex": "5", "seed": "-1", "armCount": 10, "expectedVariant": 8}, + {"clusterUnit": "market_2", "periodIndex": "9", "seed": "-1", "armCount": 10, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "10", "seed": "-1", "armCount": 10, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "11", "seed": "-1", "armCount": 10, "expectedVariant": 5}, + {"clusterUnit": "market_2", "periodIndex": "23", "seed": "-1", "armCount": 10, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "100", "seed": "-1", "armCount": 10, "expectedVariant": 7}, + {"clusterUnit": "market_2", "periodIndex": "-1", "seed": "-1", "armCount": 10, "expectedVariant": 4}, + {"clusterUnit": "market_2", "periodIndex": "-2", "seed": "-1", "armCount": 10, "expectedVariant": 7}, + {"clusterUnit": "market_2", "periodIndex": "-11", "seed": "-1", "armCount": 10, "expectedVariant": 8}, + {"clusterUnit": "market_2", "periodIndex": "-9223372036854775808", "seed": "-1", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "9223372036854775807", "seed": "-1", "armCount": 10, "expectedVariant": 9}, + {"clusterUnit": "market_2", "periodIndex": "0", "seed": "123456789", "armCount": 10, "expectedVariant": 4}, + {"clusterUnit": "market_2", "periodIndex": "1", "seed": "123456789", "armCount": 10, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "2", "seed": "123456789", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "5", "seed": "123456789", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": "market_2", "periodIndex": "9", "seed": "123456789", "armCount": 10, "expectedVariant": 5}, + {"clusterUnit": "market_2", "periodIndex": "10", "seed": "123456789", "armCount": 10, "expectedVariant": 4}, + {"clusterUnit": "market_2", "periodIndex": "11", "seed": "123456789", "armCount": 10, "expectedVariant": 5}, + {"clusterUnit": "market_2", "periodIndex": "23", "seed": "123456789", "armCount": 10, "expectedVariant": 9}, + {"clusterUnit": "market_2", "periodIndex": "100", "seed": "123456789", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": "market_2", "periodIndex": "-1", "seed": "123456789", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "-2", "seed": "123456789", "armCount": 10, "expectedVariant": 3}, + {"clusterUnit": "market_2", "periodIndex": "-11", "seed": "123456789", "armCount": 10, "expectedVariant": 4}, + {"clusterUnit": "market_2", "periodIndex": "-9223372036854775808", "seed": "123456789", "armCount": 10, "expectedVariant": 5}, + {"clusterUnit": "market_2", "periodIndex": "9223372036854775807", "seed": "123456789", "armCount": 10, "expectedVariant": 8}, + {"clusterUnit": "market_2", "periodIndex": "0", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "1", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 5}, + {"clusterUnit": "market_2", "periodIndex": "2", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": "market_2", "periodIndex": "5", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "9", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 8}, + {"clusterUnit": "market_2", "periodIndex": "10", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 4}, + {"clusterUnit": "market_2", "periodIndex": "11", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "23", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "100", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 4}, + {"clusterUnit": "market_2", "periodIndex": "-1", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": "market_2", "periodIndex": "-2", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "-11", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 3}, + {"clusterUnit": "market_2", "periodIndex": "-9223372036854775808", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 7}, + {"clusterUnit": "market_2", "periodIndex": "9223372036854775807", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "0", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "1", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": "market_2", "periodIndex": "2", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 5}, + {"clusterUnit": "market_2", "periodIndex": "5", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 7}, + {"clusterUnit": "market_2", "periodIndex": "9", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": "market_2", "periodIndex": "10", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 3}, + {"clusterUnit": "market_2", "periodIndex": "11", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 7}, + {"clusterUnit": "market_2", "periodIndex": "23", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": "market_2", "periodIndex": "100", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 4}, + {"clusterUnit": "market_2", "periodIndex": "-1", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 1}, + {"clusterUnit": "market_2", "periodIndex": "-2", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 8}, + {"clusterUnit": "market_2", "periodIndex": "-11", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 7}, + {"clusterUnit": "market_2", "periodIndex": "-9223372036854775808", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 4}, + {"clusterUnit": "market_2", "periodIndex": "9223372036854775807", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 9}, + {"clusterUnit": 123456789, "periodIndex": "0", "seed": "0", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "1", "seed": "0", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "2", "seed": "0", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "5", "seed": "0", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "9", "seed": "0", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "10", "seed": "0", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "11", "seed": "0", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "23", "seed": "0", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "100", "seed": "0", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "-1", "seed": "0", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "-2", "seed": "0", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "-11", "seed": "0", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "-9223372036854775808", "seed": "0", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "9223372036854775807", "seed": "0", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "0", "seed": "1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "1", "seed": "1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "2", "seed": "1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "5", "seed": "1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "9", "seed": "1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "10", "seed": "1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "11", "seed": "1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "23", "seed": "1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "100", "seed": "1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "-1", "seed": "1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "-2", "seed": "1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "-11", "seed": "1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "-9223372036854775808", "seed": "1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "9223372036854775807", "seed": "1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "0", "seed": "42", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "1", "seed": "42", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "2", "seed": "42", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "5", "seed": "42", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "9", "seed": "42", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "10", "seed": "42", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "11", "seed": "42", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "23", "seed": "42", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "100", "seed": "42", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "-1", "seed": "42", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "-2", "seed": "42", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "-11", "seed": "42", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "-9223372036854775808", "seed": "42", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "9223372036854775807", "seed": "42", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "0", "seed": "-1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "1", "seed": "-1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "2", "seed": "-1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "5", "seed": "-1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "9", "seed": "-1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "10", "seed": "-1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "11", "seed": "-1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "23", "seed": "-1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "100", "seed": "-1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "-1", "seed": "-1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "-2", "seed": "-1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "-11", "seed": "-1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "-9223372036854775808", "seed": "-1", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "9223372036854775807", "seed": "-1", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "0", "seed": "123456789", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "1", "seed": "123456789", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "2", "seed": "123456789", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "5", "seed": "123456789", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "9", "seed": "123456789", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "10", "seed": "123456789", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "11", "seed": "123456789", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "23", "seed": "123456789", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "100", "seed": "123456789", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "-1", "seed": "123456789", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "-2", "seed": "123456789", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "-11", "seed": "123456789", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "-9223372036854775808", "seed": "123456789", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "9223372036854775807", "seed": "123456789", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "0", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "1", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "2", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "5", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "9", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "10", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "11", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "23", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "100", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "-1", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "-2", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "-11", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "-9223372036854775808", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "9223372036854775807", "seed": "-9223372036854775808", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "0", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "1", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "2", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "5", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "9", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "10", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "11", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "23", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "100", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "-1", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "-2", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "-11", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "-9223372036854775808", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "9223372036854775807", "seed": "9223372036854775807", "armCount": 2, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "0", "seed": "0", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "1", "seed": "0", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "2", "seed": "0", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "5", "seed": "0", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "9", "seed": "0", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "10", "seed": "0", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "11", "seed": "0", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "23", "seed": "0", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "100", "seed": "0", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "-1", "seed": "0", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "-2", "seed": "0", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "-11", "seed": "0", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "-9223372036854775808", "seed": "0", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "9223372036854775807", "seed": "0", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "0", "seed": "1", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "1", "seed": "1", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "2", "seed": "1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "5", "seed": "1", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "9", "seed": "1", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "10", "seed": "1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "11", "seed": "1", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "23", "seed": "1", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "100", "seed": "1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "-1", "seed": "1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "-2", "seed": "1", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "-11", "seed": "1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "-9223372036854775808", "seed": "1", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "9223372036854775807", "seed": "1", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "0", "seed": "42", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "1", "seed": "42", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "2", "seed": "42", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "5", "seed": "42", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "9", "seed": "42", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "10", "seed": "42", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "11", "seed": "42", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "23", "seed": "42", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "100", "seed": "42", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "-1", "seed": "42", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "-2", "seed": "42", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "-11", "seed": "42", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "-9223372036854775808", "seed": "42", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "9223372036854775807", "seed": "42", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "0", "seed": "-1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "1", "seed": "-1", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "2", "seed": "-1", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "5", "seed": "-1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "9", "seed": "-1", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "10", "seed": "-1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "11", "seed": "-1", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "23", "seed": "-1", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "100", "seed": "-1", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "-1", "seed": "-1", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "-2", "seed": "-1", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "-11", "seed": "-1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "-9223372036854775808", "seed": "-1", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "9223372036854775807", "seed": "-1", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "0", "seed": "123456789", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "1", "seed": "123456789", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "2", "seed": "123456789", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "5", "seed": "123456789", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "9", "seed": "123456789", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "10", "seed": "123456789", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "11", "seed": "123456789", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "23", "seed": "123456789", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "100", "seed": "123456789", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "-1", "seed": "123456789", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "-2", "seed": "123456789", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "-11", "seed": "123456789", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "-9223372036854775808", "seed": "123456789", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "9223372036854775807", "seed": "123456789", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "0", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "1", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "2", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "5", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "9", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "10", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "11", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "23", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "100", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "-1", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "-2", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "-11", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "-9223372036854775808", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "9223372036854775807", "seed": "-9223372036854775808", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "0", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "1", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "2", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "5", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "9", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "10", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "11", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "23", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "100", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "-1", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "-2", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "-11", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "-9223372036854775808", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "9223372036854775807", "seed": "9223372036854775807", "armCount": 3, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "0", "seed": "0", "armCount": 10, "expectedVariant": 9}, + {"clusterUnit": 123456789, "periodIndex": "1", "seed": "0", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "2", "seed": "0", "armCount": 10, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "5", "seed": "0", "armCount": 10, "expectedVariant": 4}, + {"clusterUnit": 123456789, "periodIndex": "9", "seed": "0", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": 123456789, "periodIndex": "10", "seed": "0", "armCount": 10, "expectedVariant": 4}, + {"clusterUnit": 123456789, "periodIndex": "11", "seed": "0", "armCount": 10, "expectedVariant": 9}, + {"clusterUnit": 123456789, "periodIndex": "23", "seed": "0", "armCount": 10, "expectedVariant": 8}, + {"clusterUnit": 123456789, "periodIndex": "100", "seed": "0", "armCount": 10, "expectedVariant": 9}, + {"clusterUnit": 123456789, "periodIndex": "-1", "seed": "0", "armCount": 10, "expectedVariant": 4}, + {"clusterUnit": 123456789, "periodIndex": "-2", "seed": "0", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "-11", "seed": "0", "armCount": 10, "expectedVariant": 7}, + {"clusterUnit": 123456789, "periodIndex": "-9223372036854775808", "seed": "0", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": 123456789, "periodIndex": "9223372036854775807", "seed": "0", "armCount": 10, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "0", "seed": "1", "armCount": 10, "expectedVariant": 9}, + {"clusterUnit": 123456789, "periodIndex": "1", "seed": "1", "armCount": 10, "expectedVariant": 7}, + {"clusterUnit": 123456789, "periodIndex": "2", "seed": "1", "armCount": 10, "expectedVariant": 4}, + {"clusterUnit": 123456789, "periodIndex": "5", "seed": "1", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "9", "seed": "1", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "10", "seed": "1", "armCount": 10, "expectedVariant": 9}, + {"clusterUnit": 123456789, "periodIndex": "11", "seed": "1", "armCount": 10, "expectedVariant": 3}, + {"clusterUnit": 123456789, "periodIndex": "23", "seed": "1", "armCount": 10, "expectedVariant": 8}, + {"clusterUnit": 123456789, "periodIndex": "100", "seed": "1", "armCount": 10, "expectedVariant": 5}, + {"clusterUnit": 123456789, "periodIndex": "-1", "seed": "1", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "-2", "seed": "1", "armCount": 10, "expectedVariant": 5}, + {"clusterUnit": 123456789, "periodIndex": "-11", "seed": "1", "armCount": 10, "expectedVariant": 4}, + {"clusterUnit": 123456789, "periodIndex": "-9223372036854775808", "seed": "1", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "9223372036854775807", "seed": "1", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "0", "seed": "42", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": 123456789, "periodIndex": "1", "seed": "42", "armCount": 10, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "2", "seed": "42", "armCount": 10, "expectedVariant": 9}, + {"clusterUnit": 123456789, "periodIndex": "5", "seed": "42", "armCount": 10, "expectedVariant": 8}, + {"clusterUnit": 123456789, "periodIndex": "9", "seed": "42", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "10", "seed": "42", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "11", "seed": "42", "armCount": 10, "expectedVariant": 8}, + {"clusterUnit": 123456789, "periodIndex": "23", "seed": "42", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "100", "seed": "42", "armCount": 10, "expectedVariant": 9}, + {"clusterUnit": 123456789, "periodIndex": "-1", "seed": "42", "armCount": 10, "expectedVariant": 4}, + {"clusterUnit": 123456789, "periodIndex": "-2", "seed": "42", "armCount": 10, "expectedVariant": 3}, + {"clusterUnit": 123456789, "periodIndex": "-11", "seed": "42", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": 123456789, "periodIndex": "-9223372036854775808", "seed": "42", "armCount": 10, "expectedVariant": 7}, + {"clusterUnit": 123456789, "periodIndex": "9223372036854775807", "seed": "42", "armCount": 10, "expectedVariant": 4}, + {"clusterUnit": 123456789, "periodIndex": "0", "seed": "-1", "armCount": 10, "expectedVariant": 8}, + {"clusterUnit": 123456789, "periodIndex": "1", "seed": "-1", "armCount": 10, "expectedVariant": 4}, + {"clusterUnit": 123456789, "periodIndex": "2", "seed": "-1", "armCount": 10, "expectedVariant": 5}, + {"clusterUnit": 123456789, "periodIndex": "5", "seed": "-1", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "9", "seed": "-1", "armCount": 10, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "10", "seed": "-1", "armCount": 10, "expectedVariant": 8}, + {"clusterUnit": 123456789, "periodIndex": "11", "seed": "-1", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "23", "seed": "-1", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": 123456789, "periodIndex": "100", "seed": "-1", "armCount": 10, "expectedVariant": 7}, + {"clusterUnit": 123456789, "periodIndex": "-1", "seed": "-1", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "-2", "seed": "-1", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "-11", "seed": "-1", "armCount": 10, "expectedVariant": 9}, + {"clusterUnit": 123456789, "periodIndex": "-9223372036854775808", "seed": "-1", "armCount": 10, "expectedVariant": 7}, + {"clusterUnit": 123456789, "periodIndex": "9223372036854775807", "seed": "-1", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": 123456789, "periodIndex": "0", "seed": "123456789", "armCount": 10, "expectedVariant": 9}, + {"clusterUnit": 123456789, "periodIndex": "1", "seed": "123456789", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "2", "seed": "123456789", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "5", "seed": "123456789", "armCount": 10, "expectedVariant": 4}, + {"clusterUnit": 123456789, "periodIndex": "9", "seed": "123456789", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": 123456789, "periodIndex": "10", "seed": "123456789", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": 123456789, "periodIndex": "11", "seed": "123456789", "armCount": 10, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "23", "seed": "123456789", "armCount": 10, "expectedVariant": 8}, + {"clusterUnit": 123456789, "periodIndex": "100", "seed": "123456789", "armCount": 10, "expectedVariant": 3}, + {"clusterUnit": 123456789, "periodIndex": "-1", "seed": "123456789", "armCount": 10, "expectedVariant": 3}, + {"clusterUnit": 123456789, "periodIndex": "-2", "seed": "123456789", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": 123456789, "periodIndex": "-11", "seed": "123456789", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "-9223372036854775808", "seed": "123456789", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "9223372036854775807", "seed": "123456789", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "0", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 9}, + {"clusterUnit": 123456789, "periodIndex": "1", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "2", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 8}, + {"clusterUnit": 123456789, "periodIndex": "5", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 5}, + {"clusterUnit": 123456789, "periodIndex": "9", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "10", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "11", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 5}, + {"clusterUnit": 123456789, "periodIndex": "23", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "100", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 7}, + {"clusterUnit": 123456789, "periodIndex": "-1", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 3}, + {"clusterUnit": 123456789, "periodIndex": "-2", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "-11", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 5}, + {"clusterUnit": 123456789, "periodIndex": "-9223372036854775808", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 1}, + {"clusterUnit": 123456789, "periodIndex": "9223372036854775807", "seed": "-9223372036854775808", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "0", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 5}, + {"clusterUnit": 123456789, "periodIndex": "1", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": 123456789, "periodIndex": "2", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 8}, + {"clusterUnit": 123456789, "periodIndex": "5", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 9}, + {"clusterUnit": 123456789, "periodIndex": "9", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "10", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 4}, + {"clusterUnit": 123456789, "periodIndex": "11", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 8}, + {"clusterUnit": 123456789, "periodIndex": "23", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": 123456789, "periodIndex": "100", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 2}, + {"clusterUnit": 123456789, "periodIndex": "-1", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 4}, + {"clusterUnit": 123456789, "periodIndex": "-2", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 0}, + {"clusterUnit": 123456789, "periodIndex": "-11", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 9}, + {"clusterUnit": 123456789, "periodIndex": "-9223372036854775808", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 6}, + {"clusterUnit": 123456789, "periodIndex": "9223372036854775807", "seed": "9223372036854775807", "armCount": 10, "expectedVariant": 2} + ], + "fisherYatesPermutation": [ + {"clusterUnit": "market_1", "cycleIndex": "0", "seed": "0", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "1", "seed": "0", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "2", "seed": "0", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "-1", "seed": "0", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "-2", "seed": "0", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "100", "seed": "0", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "-9223372036854775808", "seed": "0", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "9223372036854775807", "seed": "0", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "0", "seed": "1", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "1", "seed": "1", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "2", "seed": "1", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "-1", "seed": "1", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "-2", "seed": "1", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "100", "seed": "1", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "-9223372036854775808", "seed": "1", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "9223372036854775807", "seed": "1", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "0", "seed": "42", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "1", "seed": "42", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "2", "seed": "42", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "-1", "seed": "42", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "-2", "seed": "42", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "100", "seed": "42", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "-9223372036854775808", "seed": "42", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "9223372036854775807", "seed": "42", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "0", "seed": "-1", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "1", "seed": "-1", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "2", "seed": "-1", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "-1", "seed": "-1", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "-2", "seed": "-1", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "100", "seed": "-1", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "-9223372036854775808", "seed": "-1", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "9223372036854775807", "seed": "-1", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "0", "seed": "123456789", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "1", "seed": "123456789", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "2", "seed": "123456789", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "-1", "seed": "123456789", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "-2", "seed": "123456789", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "100", "seed": "123456789", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "-9223372036854775808", "seed": "123456789", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "9223372036854775807", "seed": "123456789", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "0", "seed": "-9223372036854775808", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "1", "seed": "-9223372036854775808", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "2", "seed": "-9223372036854775808", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "-1", "seed": "-9223372036854775808", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "-2", "seed": "-9223372036854775808", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "100", "seed": "-9223372036854775808", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "-9223372036854775808", "seed": "-9223372036854775808", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "9223372036854775807", "seed": "-9223372036854775808", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "0", "seed": "9223372036854775807", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "1", "seed": "9223372036854775807", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "2", "seed": "9223372036854775807", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "-1", "seed": "9223372036854775807", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "-2", "seed": "9223372036854775807", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "100", "seed": "9223372036854775807", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "-9223372036854775808", "seed": "9223372036854775807", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "9223372036854775807", "seed": "9223372036854775807", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "0", "seed": "0", "armCount": 3, "expectedPermutation": [0, 2, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "1", "seed": "0", "armCount": 3, "expectedPermutation": [2, 0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "2", "seed": "0", "armCount": 3, "expectedPermutation": [2, 0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "-1", "seed": "0", "armCount": 3, "expectedPermutation": [0, 2, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "-2", "seed": "0", "armCount": 3, "expectedPermutation": [2, 1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "100", "seed": "0", "armCount": 3, "expectedPermutation": [1, 2, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "-9223372036854775808", "seed": "0", "armCount": 3, "expectedPermutation": [2, 0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "9223372036854775807", "seed": "0", "armCount": 3, "expectedPermutation": [2, 0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "0", "seed": "1", "armCount": 3, "expectedPermutation": [1, 2, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "1", "seed": "1", "armCount": 3, "expectedPermutation": [0, 2, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "2", "seed": "1", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": "market_1", "cycleIndex": "-1", "seed": "1", "armCount": 3, "expectedPermutation": [2, 0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "-2", "seed": "1", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": "market_1", "cycleIndex": "100", "seed": "1", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": "market_1", "cycleIndex": "-9223372036854775808", "seed": "1", "armCount": 3, "expectedPermutation": [2, 1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "9223372036854775807", "seed": "1", "armCount": 3, "expectedPermutation": [0, 2, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "0", "seed": "42", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": "market_1", "cycleIndex": "1", "seed": "42", "armCount": 3, "expectedPermutation": [0, 2, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "2", "seed": "42", "armCount": 3, "expectedPermutation": [1, 0, 2]}, + {"clusterUnit": "market_1", "cycleIndex": "-1", "seed": "42", "armCount": 3, "expectedPermutation": [1, 2, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "-2", "seed": "42", "armCount": 3, "expectedPermutation": [1, 0, 2]}, + {"clusterUnit": "market_1", "cycleIndex": "100", "seed": "42", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": "market_1", "cycleIndex": "-9223372036854775808", "seed": "42", "armCount": 3, "expectedPermutation": [2, 1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "9223372036854775807", "seed": "42", "armCount": 3, "expectedPermutation": [2, 1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "0", "seed": "-1", "armCount": 3, "expectedPermutation": [1, 2, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "1", "seed": "-1", "armCount": 3, "expectedPermutation": [2, 0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "2", "seed": "-1", "armCount": 3, "expectedPermutation": [2, 0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "-1", "seed": "-1", "armCount": 3, "expectedPermutation": [2, 0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "-2", "seed": "-1", "armCount": 3, "expectedPermutation": [1, 0, 2]}, + {"clusterUnit": "market_1", "cycleIndex": "100", "seed": "-1", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": "market_1", "cycleIndex": "-9223372036854775808", "seed": "-1", "armCount": 3, "expectedPermutation": [2, 1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "9223372036854775807", "seed": "-1", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": "market_1", "cycleIndex": "0", "seed": "123456789", "armCount": 3, "expectedPermutation": [1, 2, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "1", "seed": "123456789", "armCount": 3, "expectedPermutation": [0, 2, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "2", "seed": "123456789", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": "market_1", "cycleIndex": "-1", "seed": "123456789", "armCount": 3, "expectedPermutation": [1, 0, 2]}, + {"clusterUnit": "market_1", "cycleIndex": "-2", "seed": "123456789", "armCount": 3, "expectedPermutation": [1, 0, 2]}, + {"clusterUnit": "market_1", "cycleIndex": "100", "seed": "123456789", "armCount": 3, "expectedPermutation": [1, 2, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "-9223372036854775808", "seed": "123456789", "armCount": 3, "expectedPermutation": [1, 0, 2]}, + {"clusterUnit": "market_1", "cycleIndex": "9223372036854775807", "seed": "123456789", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": "market_1", "cycleIndex": "0", "seed": "-9223372036854775808", "armCount": 3, "expectedPermutation": [1, 0, 2]}, + {"clusterUnit": "market_1", "cycleIndex": "1", "seed": "-9223372036854775808", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": "market_1", "cycleIndex": "2", "seed": "-9223372036854775808", "armCount": 3, "expectedPermutation": [1, 2, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "-1", "seed": "-9223372036854775808", "armCount": 3, "expectedPermutation": [1, 0, 2]}, + {"clusterUnit": "market_1", "cycleIndex": "-2", "seed": "-9223372036854775808", "armCount": 3, "expectedPermutation": [1, 0, 2]}, + {"clusterUnit": "market_1", "cycleIndex": "100", "seed": "-9223372036854775808", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": "market_1", "cycleIndex": "-9223372036854775808", "seed": "-9223372036854775808", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": "market_1", "cycleIndex": "9223372036854775807", "seed": "-9223372036854775808", "armCount": 3, "expectedPermutation": [0, 2, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "0", "seed": "9223372036854775807", "armCount": 3, "expectedPermutation": [2, 1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "1", "seed": "9223372036854775807", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": "market_1", "cycleIndex": "2", "seed": "9223372036854775807", "armCount": 3, "expectedPermutation": [2, 0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "-1", "seed": "9223372036854775807", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": "market_1", "cycleIndex": "-2", "seed": "9223372036854775807", "armCount": 3, "expectedPermutation": [2, 1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "100", "seed": "9223372036854775807", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": "market_1", "cycleIndex": "-9223372036854775808", "seed": "9223372036854775807", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": "market_1", "cycleIndex": "9223372036854775807", "seed": "9223372036854775807", "armCount": 3, "expectedPermutation": [2, 0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "0", "seed": "0", "armCount": 10, "expectedPermutation": [0, 3, 7, 5, 8, 2, 6, 4, 1, 9]}, + {"clusterUnit": "market_1", "cycleIndex": "1", "seed": "0", "armCount": 10, "expectedPermutation": [6, 0, 3, 8, 9, 7, 2, 4, 5, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "2", "seed": "0", "armCount": 10, "expectedPermutation": [3, 0, 4, 8, 1, 6, 9, 2, 7, 5]}, + {"clusterUnit": "market_1", "cycleIndex": "-1", "seed": "0", "armCount": 10, "expectedPermutation": [0, 2, 7, 9, 1, 6, 4, 5, 3, 8]}, + {"clusterUnit": "market_1", "cycleIndex": "-2", "seed": "0", "armCount": 10, "expectedPermutation": [6, 9, 8, 3, 4, 7, 2, 5, 0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "100", "seed": "0", "armCount": 10, "expectedPermutation": [1, 7, 8, 3, 9, 5, 0, 4, 6, 2]}, + {"clusterUnit": "market_1", "cycleIndex": "-9223372036854775808", "seed": "0", "armCount": 10, "expectedPermutation": [8, 9, 3, 6, 4, 1, 5, 0, 2, 7]}, + {"clusterUnit": "market_1", "cycleIndex": "9223372036854775807", "seed": "0", "armCount": 10, "expectedPermutation": [6, 4, 3, 1, 0, 7, 8, 9, 5, 2]}, + {"clusterUnit": "market_1", "cycleIndex": "0", "seed": "1", "armCount": 10, "expectedPermutation": [9, 4, 3, 5, 8, 0, 2, 6, 7, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "1", "seed": "1", "armCount": 10, "expectedPermutation": [3, 8, 4, 0, 1, 7, 5, 6, 2, 9]}, + {"clusterUnit": "market_1", "cycleIndex": "2", "seed": "1", "armCount": 10, "expectedPermutation": [4, 1, 5, 3, 6, 7, 0, 9, 2, 8]}, + {"clusterUnit": "market_1", "cycleIndex": "-1", "seed": "1", "armCount": 10, "expectedPermutation": [7, 0, 1, 5, 2, 9, 8, 6, 4, 3]}, + {"clusterUnit": "market_1", "cycleIndex": "-2", "seed": "1", "armCount": 10, "expectedPermutation": [9, 1, 7, 3, 4, 5, 2, 6, 8, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "100", "seed": "1", "armCount": 10, "expectedPermutation": [0, 5, 7, 2, 6, 1, 9, 4, 8, 3]}, + {"clusterUnit": "market_1", "cycleIndex": "-9223372036854775808", "seed": "1", "armCount": 10, "expectedPermutation": [2, 4, 0, 7, 5, 1, 3, 8, 9, 6]}, + {"clusterUnit": "market_1", "cycleIndex": "9223372036854775807", "seed": "1", "armCount": 10, "expectedPermutation": [6, 2, 5, 3, 4, 1, 7, 8, 9, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "0", "seed": "42", "armCount": 10, "expectedPermutation": [3, 7, 2, 9, 5, 8, 0, 1, 4, 6]}, + {"clusterUnit": "market_1", "cycleIndex": "1", "seed": "42", "armCount": 10, "expectedPermutation": [0, 8, 1, 6, 3, 4, 5, 9, 7, 2]}, + {"clusterUnit": "market_1", "cycleIndex": "2", "seed": "42", "armCount": 10, "expectedPermutation": [1, 3, 2, 0, 7, 6, 8, 4, 9, 5]}, + {"clusterUnit": "market_1", "cycleIndex": "-1", "seed": "42", "armCount": 10, "expectedPermutation": [5, 7, 0, 6, 1, 4, 3, 8, 2, 9]}, + {"clusterUnit": "market_1", "cycleIndex": "-2", "seed": "42", "armCount": 10, "expectedPermutation": [6, 5, 2, 0, 1, 3, 4, 9, 8, 7]}, + {"clusterUnit": "market_1", "cycleIndex": "100", "seed": "42", "armCount": 10, "expectedPermutation": [0, 6, 9, 1, 4, 2, 3, 8, 7, 5]}, + {"clusterUnit": "market_1", "cycleIndex": "-9223372036854775808", "seed": "42", "armCount": 10, "expectedPermutation": [8, 1, 3, 6, 5, 0, 4, 9, 7, 2]}, + {"clusterUnit": "market_1", "cycleIndex": "9223372036854775807", "seed": "42", "armCount": 10, "expectedPermutation": [6, 8, 3, 5, 7, 4, 2, 0, 1, 9]}, + {"clusterUnit": "market_1", "cycleIndex": "0", "seed": "-1", "armCount": 10, "expectedPermutation": [1, 5, 7, 0, 9, 4, 2, 3, 6, 8]}, + {"clusterUnit": "market_1", "cycleIndex": "1", "seed": "-1", "armCount": 10, "expectedPermutation": [6, 4, 9, 8, 0, 3, 5, 1, 2, 7]}, + {"clusterUnit": "market_1", "cycleIndex": "2", "seed": "-1", "armCount": 10, "expectedPermutation": [4, 0, 5, 8, 9, 3, 7, 6, 1, 2]}, + {"clusterUnit": "market_1", "cycleIndex": "-1", "seed": "-1", "armCount": 10, "expectedPermutation": [6, 5, 9, 1, 0, 8, 2, 4, 7, 3]}, + {"clusterUnit": "market_1", "cycleIndex": "-2", "seed": "-1", "armCount": 10, "expectedPermutation": [6, 7, 2, 5, 8, 4, 1, 0, 3, 9]}, + {"clusterUnit": "market_1", "cycleIndex": "100", "seed": "-1", "armCount": 10, "expectedPermutation": [0, 5, 6, 3, 1, 7, 8, 4, 2, 9]}, + {"clusterUnit": "market_1", "cycleIndex": "-9223372036854775808", "seed": "-1", "armCount": 10, "expectedPermutation": [2, 5, 4, 8, 0, 9, 7, 6, 3, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "9223372036854775807", "seed": "-1", "armCount": 10, "expectedPermutation": [9, 8, 6, 3, 1, 7, 2, 0, 4, 5]}, + {"clusterUnit": "market_1", "cycleIndex": "0", "seed": "123456789", "armCount": 10, "expectedPermutation": [3, 6, 5, 1, 2, 9, 8, 0, 4, 7]}, + {"clusterUnit": "market_1", "cycleIndex": "1", "seed": "123456789", "armCount": 10, "expectedPermutation": [6, 2, 1, 7, 3, 8, 0, 4, 5, 9]}, + {"clusterUnit": "market_1", "cycleIndex": "2", "seed": "123456789", "armCount": 10, "expectedPermutation": [8, 1, 2, 5, 7, 9, 0, 6, 4, 3]}, + {"clusterUnit": "market_1", "cycleIndex": "-1", "seed": "123456789", "armCount": 10, "expectedPermutation": [8, 3, 9, 0, 7, 1, 6, 5, 4, 2]}, + {"clusterUnit": "market_1", "cycleIndex": "-2", "seed": "123456789", "armCount": 10, "expectedPermutation": [1, 9, 3, 7, 4, 8, 0, 5, 2, 6]}, + {"clusterUnit": "market_1", "cycleIndex": "100", "seed": "123456789", "armCount": 10, "expectedPermutation": [1, 4, 8, 9, 2, 3, 7, 5, 6, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "-9223372036854775808", "seed": "123456789", "armCount": 10, "expectedPermutation": [1, 0, 7, 3, 4, 6, 5, 2, 8, 9]}, + {"clusterUnit": "market_1", "cycleIndex": "9223372036854775807", "seed": "123456789", "armCount": 10, "expectedPermutation": [6, 4, 3, 7, 1, 9, 8, 2, 0, 5]}, + {"clusterUnit": "market_1", "cycleIndex": "0", "seed": "-9223372036854775808", "armCount": 10, "expectedPermutation": [3, 9, 6, 8, 4, 7, 2, 5, 1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "1", "seed": "-9223372036854775808", "armCount": 10, "expectedPermutation": [3, 1, 7, 8, 9, 0, 2, 4, 5, 6]}, + {"clusterUnit": "market_1", "cycleIndex": "2", "seed": "-9223372036854775808", "armCount": 10, "expectedPermutation": [7, 6, 8, 3, 2, 5, 4, 1, 9, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "-1", "seed": "-9223372036854775808", "armCount": 10, "expectedPermutation": [8, 3, 7, 6, 2, 9, 5, 4, 1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "-2", "seed": "-9223372036854775808", "armCount": 10, "expectedPermutation": [4, 9, 2, 7, 1, 6, 0, 8, 3, 5]}, + {"clusterUnit": "market_1", "cycleIndex": "100", "seed": "-9223372036854775808", "armCount": 10, "expectedPermutation": [4, 9, 3, 5, 8, 6, 7, 2, 0, 1]}, + {"clusterUnit": "market_1", "cycleIndex": "-9223372036854775808", "seed": "-9223372036854775808", "armCount": 10, "expectedPermutation": [0, 9, 2, 7, 1, 3, 6, 8, 4, 5]}, + {"clusterUnit": "market_1", "cycleIndex": "9223372036854775807", "seed": "-9223372036854775808", "armCount": 10, "expectedPermutation": [9, 5, 1, 0, 6, 7, 2, 4, 8, 3]}, + {"clusterUnit": "market_1", "cycleIndex": "0", "seed": "9223372036854775807", "armCount": 10, "expectedPermutation": [8, 1, 6, 3, 5, 7, 0, 4, 2, 9]}, + {"clusterUnit": "market_1", "cycleIndex": "1", "seed": "9223372036854775807", "armCount": 10, "expectedPermutation": [0, 5, 6, 3, 2, 7, 4, 9, 1, 8]}, + {"clusterUnit": "market_1", "cycleIndex": "2", "seed": "9223372036854775807", "armCount": 10, "expectedPermutation": [7, 9, 5, 3, 4, 6, 8, 2, 1, 0]}, + {"clusterUnit": "market_1", "cycleIndex": "-1", "seed": "9223372036854775807", "armCount": 10, "expectedPermutation": [0, 7, 5, 3, 6, 2, 4, 9, 1, 8]}, + {"clusterUnit": "market_1", "cycleIndex": "-2", "seed": "9223372036854775807", "armCount": 10, "expectedPermutation": [2, 5, 3, 7, 9, 1, 4, 0, 8, 6]}, + {"clusterUnit": "market_1", "cycleIndex": "100", "seed": "9223372036854775807", "armCount": 10, "expectedPermutation": [6, 1, 2, 9, 5, 0, 8, 3, 7, 4]}, + {"clusterUnit": "market_1", "cycleIndex": "-9223372036854775808", "seed": "9223372036854775807", "armCount": 10, "expectedPermutation": [7, 5, 6, 9, 4, 8, 2, 0, 1, 3]}, + {"clusterUnit": "market_1", "cycleIndex": "9223372036854775807", "seed": "9223372036854775807", "armCount": 10, "expectedPermutation": [5, 7, 8, 6, 3, 2, 0, 4, 1, 9]}, + {"clusterUnit": "market_2", "cycleIndex": "0", "seed": "0", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "1", "seed": "0", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "2", "seed": "0", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "-1", "seed": "0", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "-2", "seed": "0", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "100", "seed": "0", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "-9223372036854775808", "seed": "0", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "9223372036854775807", "seed": "0", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "0", "seed": "1", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "1", "seed": "1", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "2", "seed": "1", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "-1", "seed": "1", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "-2", "seed": "1", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "100", "seed": "1", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "-9223372036854775808", "seed": "1", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "9223372036854775807", "seed": "1", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "0", "seed": "42", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "1", "seed": "42", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "2", "seed": "42", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "-1", "seed": "42", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "-2", "seed": "42", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "100", "seed": "42", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "-9223372036854775808", "seed": "42", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "9223372036854775807", "seed": "42", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "0", "seed": "-1", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "1", "seed": "-1", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "2", "seed": "-1", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "-1", "seed": "-1", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "-2", "seed": "-1", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "100", "seed": "-1", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "-9223372036854775808", "seed": "-1", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "9223372036854775807", "seed": "-1", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "0", "seed": "123456789", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "1", "seed": "123456789", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "2", "seed": "123456789", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "-1", "seed": "123456789", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "-2", "seed": "123456789", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "100", "seed": "123456789", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "-9223372036854775808", "seed": "123456789", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "9223372036854775807", "seed": "123456789", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "0", "seed": "-9223372036854775808", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "1", "seed": "-9223372036854775808", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "2", "seed": "-9223372036854775808", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "-1", "seed": "-9223372036854775808", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "-2", "seed": "-9223372036854775808", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "100", "seed": "-9223372036854775808", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "-9223372036854775808", "seed": "-9223372036854775808", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "9223372036854775807", "seed": "-9223372036854775808", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "0", "seed": "9223372036854775807", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "1", "seed": "9223372036854775807", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "2", "seed": "9223372036854775807", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "-1", "seed": "9223372036854775807", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "-2", "seed": "9223372036854775807", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "100", "seed": "9223372036854775807", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "-9223372036854775808", "seed": "9223372036854775807", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "9223372036854775807", "seed": "9223372036854775807", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "0", "seed": "0", "armCount": 3, "expectedPermutation": [1, 0, 2]}, + {"clusterUnit": "market_2", "cycleIndex": "1", "seed": "0", "armCount": 3, "expectedPermutation": [1, 2, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "2", "seed": "0", "armCount": 3, "expectedPermutation": [2, 1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "-1", "seed": "0", "armCount": 3, "expectedPermutation": [1, 2, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "-2", "seed": "0", "armCount": 3, "expectedPermutation": [0, 2, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "100", "seed": "0", "armCount": 3, "expectedPermutation": [1, 0, 2]}, + {"clusterUnit": "market_2", "cycleIndex": "-9223372036854775808", "seed": "0", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": "market_2", "cycleIndex": "9223372036854775807", "seed": "0", "armCount": 3, "expectedPermutation": [2, 1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "0", "seed": "1", "armCount": 3, "expectedPermutation": [0, 2, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "1", "seed": "1", "armCount": 3, "expectedPermutation": [1, 0, 2]}, + {"clusterUnit": "market_2", "cycleIndex": "2", "seed": "1", "armCount": 3, "expectedPermutation": [2, 0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "-1", "seed": "1", "armCount": 3, "expectedPermutation": [2, 0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "-2", "seed": "1", "armCount": 3, "expectedPermutation": [2, 1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "100", "seed": "1", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": "market_2", "cycleIndex": "-9223372036854775808", "seed": "1", "armCount": 3, "expectedPermutation": [2, 0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "9223372036854775807", "seed": "1", "armCount": 3, "expectedPermutation": [1, 2, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "0", "seed": "42", "armCount": 3, "expectedPermutation": [0, 2, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "1", "seed": "42", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": "market_2", "cycleIndex": "2", "seed": "42", "armCount": 3, "expectedPermutation": [1, 0, 2]}, + {"clusterUnit": "market_2", "cycleIndex": "-1", "seed": "42", "armCount": 3, "expectedPermutation": [1, 2, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "-2", "seed": "42", "armCount": 3, "expectedPermutation": [2, 1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "100", "seed": "42", "armCount": 3, "expectedPermutation": [1, 2, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "-9223372036854775808", "seed": "42", "armCount": 3, "expectedPermutation": [2, 1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "9223372036854775807", "seed": "42", "armCount": 3, "expectedPermutation": [0, 2, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "0", "seed": "-1", "armCount": 3, "expectedPermutation": [0, 2, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "1", "seed": "-1", "armCount": 3, "expectedPermutation": [1, 0, 2]}, + {"clusterUnit": "market_2", "cycleIndex": "2", "seed": "-1", "armCount": 3, "expectedPermutation": [0, 2, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "-1", "seed": "-1", "armCount": 3, "expectedPermutation": [1, 0, 2]}, + {"clusterUnit": "market_2", "cycleIndex": "-2", "seed": "-1", "armCount": 3, "expectedPermutation": [2, 0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "100", "seed": "-1", "armCount": 3, "expectedPermutation": [1, 0, 2]}, + {"clusterUnit": "market_2", "cycleIndex": "-9223372036854775808", "seed": "-1", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": "market_2", "cycleIndex": "9223372036854775807", "seed": "-1", "armCount": 3, "expectedPermutation": [0, 2, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "0", "seed": "123456789", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": "market_2", "cycleIndex": "1", "seed": "123456789", "armCount": 3, "expectedPermutation": [1, 0, 2]}, + {"clusterUnit": "market_2", "cycleIndex": "2", "seed": "123456789", "armCount": 3, "expectedPermutation": [0, 2, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "-1", "seed": "123456789", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": "market_2", "cycleIndex": "-2", "seed": "123456789", "armCount": 3, "expectedPermutation": [0, 2, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "100", "seed": "123456789", "armCount": 3, "expectedPermutation": [1, 2, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "-9223372036854775808", "seed": "123456789", "armCount": 3, "expectedPermutation": [2, 0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "9223372036854775807", "seed": "123456789", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": "market_2", "cycleIndex": "0", "seed": "-9223372036854775808", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": "market_2", "cycleIndex": "1", "seed": "-9223372036854775808", "armCount": 3, "expectedPermutation": [1, 2, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "2", "seed": "-9223372036854775808", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": "market_2", "cycleIndex": "-1", "seed": "-9223372036854775808", "armCount": 3, "expectedPermutation": [0, 2, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "-2", "seed": "-9223372036854775808", "armCount": 3, "expectedPermutation": [2, 0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "100", "seed": "-9223372036854775808", "armCount": 3, "expectedPermutation": [2, 0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "-9223372036854775808", "seed": "-9223372036854775808", "armCount": 3, "expectedPermutation": [0, 2, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "9223372036854775807", "seed": "-9223372036854775808", "armCount": 3, "expectedPermutation": [0, 2, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "0", "seed": "9223372036854775807", "armCount": 3, "expectedPermutation": [2, 0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "1", "seed": "9223372036854775807", "armCount": 3, "expectedPermutation": [2, 1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "2", "seed": "9223372036854775807", "armCount": 3, "expectedPermutation": [2, 1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "-1", "seed": "9223372036854775807", "armCount": 3, "expectedPermutation": [1, 0, 2]}, + {"clusterUnit": "market_2", "cycleIndex": "-2", "seed": "9223372036854775807", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": "market_2", "cycleIndex": "100", "seed": "9223372036854775807", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": "market_2", "cycleIndex": "-9223372036854775808", "seed": "9223372036854775807", "armCount": 3, "expectedPermutation": [0, 2, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "9223372036854775807", "seed": "9223372036854775807", "armCount": 3, "expectedPermutation": [2, 0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "0", "seed": "0", "armCount": 10, "expectedPermutation": [4, 6, 2, 7, 5, 8, 9, 0, 1, 3]}, + {"clusterUnit": "market_2", "cycleIndex": "1", "seed": "0", "armCount": 10, "expectedPermutation": [1, 2, 5, 9, 8, 6, 0, 3, 4, 7]}, + {"clusterUnit": "market_2", "cycleIndex": "2", "seed": "0", "armCount": 10, "expectedPermutation": [8, 4, 6, 3, 1, 2, 0, 5, 9, 7]}, + {"clusterUnit": "market_2", "cycleIndex": "-1", "seed": "0", "armCount": 10, "expectedPermutation": [5, 2, 3, 0, 4, 6, 7, 1, 9, 8]}, + {"clusterUnit": "market_2", "cycleIndex": "-2", "seed": "0", "armCount": 10, "expectedPermutation": [9, 7, 8, 2, 1, 6, 0, 3, 4, 5]}, + {"clusterUnit": "market_2", "cycleIndex": "100", "seed": "0", "armCount": 10, "expectedPermutation": [8, 6, 9, 3, 4, 2, 0, 1, 7, 5]}, + {"clusterUnit": "market_2", "cycleIndex": "-9223372036854775808", "seed": "0", "armCount": 10, "expectedPermutation": [0, 9, 5, 4, 1, 7, 8, 6, 2, 3]}, + {"clusterUnit": "market_2", "cycleIndex": "9223372036854775807", "seed": "0", "armCount": 10, "expectedPermutation": [5, 8, 0, 2, 9, 7, 6, 3, 4, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "0", "seed": "1", "armCount": 10, "expectedPermutation": [4, 2, 5, 9, 0, 7, 8, 1, 3, 6]}, + {"clusterUnit": "market_2", "cycleIndex": "1", "seed": "1", "armCount": 10, "expectedPermutation": [1, 7, 9, 2, 6, 0, 4, 5, 3, 8]}, + {"clusterUnit": "market_2", "cycleIndex": "2", "seed": "1", "armCount": 10, "expectedPermutation": [2, 8, 7, 5, 6, 9, 3, 4, 0, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "-1", "seed": "1", "armCount": 10, "expectedPermutation": [2, 3, 4, 9, 6, 7, 8, 5, 1, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "-2", "seed": "1", "armCount": 10, "expectedPermutation": [2, 3, 8, 5, 9, 1, 7, 6, 4, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "100", "seed": "1", "armCount": 10, "expectedPermutation": [4, 6, 2, 3, 0, 5, 9, 1, 8, 7]}, + {"clusterUnit": "market_2", "cycleIndex": "-9223372036854775808", "seed": "1", "armCount": 10, "expectedPermutation": [8, 6, 1, 2, 5, 9, 4, 0, 3, 7]}, + {"clusterUnit": "market_2", "cycleIndex": "9223372036854775807", "seed": "1", "armCount": 10, "expectedPermutation": [7, 5, 0, 9, 1, 2, 6, 4, 3, 8]}, + {"clusterUnit": "market_2", "cycleIndex": "0", "seed": "42", "armCount": 10, "expectedPermutation": [9, 2, 3, 8, 0, 6, 5, 1, 7, 4]}, + {"clusterUnit": "market_2", "cycleIndex": "1", "seed": "42", "armCount": 10, "expectedPermutation": [0, 9, 7, 3, 5, 6, 2, 4, 1, 8]}, + {"clusterUnit": "market_2", "cycleIndex": "2", "seed": "42", "armCount": 10, "expectedPermutation": [4, 0, 3, 8, 1, 6, 9, 2, 7, 5]}, + {"clusterUnit": "market_2", "cycleIndex": "-1", "seed": "42", "armCount": 10, "expectedPermutation": [5, 9, 3, 0, 4, 8, 6, 7, 1, 2]}, + {"clusterUnit": "market_2", "cycleIndex": "-2", "seed": "42", "armCount": 10, "expectedPermutation": [7, 8, 0, 9, 6, 2, 5, 4, 3, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "100", "seed": "42", "armCount": 10, "expectedPermutation": [1, 3, 5, 8, 6, 4, 0, 9, 7, 2]}, + {"clusterUnit": "market_2", "cycleIndex": "-9223372036854775808", "seed": "42", "armCount": 10, "expectedPermutation": [3, 1, 0, 8, 2, 7, 6, 4, 5, 9]}, + {"clusterUnit": "market_2", "cycleIndex": "9223372036854775807", "seed": "42", "armCount": 10, "expectedPermutation": [0, 9, 6, 5, 2, 1, 7, 3, 8, 4]}, + {"clusterUnit": "market_2", "cycleIndex": "0", "seed": "-1", "armCount": 10, "expectedPermutation": [0, 6, 9, 7, 2, 8, 3, 4, 5, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "1", "seed": "-1", "armCount": 10, "expectedPermutation": [1, 5, 2, 9, 3, 8, 6, 7, 4, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "2", "seed": "-1", "armCount": 10, "expectedPermutation": [8, 7, 3, 1, 2, 9, 6, 4, 5, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "-1", "seed": "-1", "armCount": 10, "expectedPermutation": [3, 0, 6, 1, 9, 2, 5, 8, 7, 4]}, + {"clusterUnit": "market_2", "cycleIndex": "-2", "seed": "-1", "armCount": 10, "expectedPermutation": [9, 5, 1, 4, 3, 7, 6, 0, 2, 8]}, + {"clusterUnit": "market_2", "cycleIndex": "100", "seed": "-1", "armCount": 10, "expectedPermutation": [5, 6, 9, 7, 8, 1, 0, 4, 2, 3]}, + {"clusterUnit": "market_2", "cycleIndex": "-9223372036854775808", "seed": "-1", "armCount": 10, "expectedPermutation": [0, 8, 2, 4, 6, 3, 5, 1, 9, 7]}, + {"clusterUnit": "market_2", "cycleIndex": "9223372036854775807", "seed": "-1", "armCount": 10, "expectedPermutation": [4, 3, 8, 6, 0, 2, 5, 9, 7, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "0", "seed": "123456789", "armCount": 10, "expectedPermutation": [4, 1, 2, 7, 0, 6, 9, 8, 3, 5]}, + {"clusterUnit": "market_2", "cycleIndex": "1", "seed": "123456789", "armCount": 10, "expectedPermutation": [4, 5, 3, 9, 1, 7, 6, 0, 8, 2]}, + {"clusterUnit": "market_2", "cycleIndex": "2", "seed": "123456789", "armCount": 10, "expectedPermutation": [7, 8, 6, 9, 0, 3, 5, 4, 2, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "-1", "seed": "123456789", "armCount": 10, "expectedPermutation": [6, 8, 2, 5, 7, 4, 9, 1, 3, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "-2", "seed": "123456789", "armCount": 10, "expectedPermutation": [6, 5, 7, 8, 0, 2, 3, 1, 9, 4]}, + {"clusterUnit": "market_2", "cycleIndex": "100", "seed": "123456789", "armCount": 10, "expectedPermutation": [3, 6, 4, 5, 0, 7, 9, 8, 1, 2]}, + {"clusterUnit": "market_2", "cycleIndex": "-9223372036854775808", "seed": "123456789", "armCount": 10, "expectedPermutation": [8, 6, 3, 4, 7, 9, 5, 1, 2, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "9223372036854775807", "seed": "123456789", "armCount": 10, "expectedPermutation": [0, 1, 8, 9, 2, 6, 4, 5, 3, 7]}, + {"clusterUnit": "market_2", "cycleIndex": "0", "seed": "-9223372036854775808", "armCount": 10, "expectedPermutation": [0, 5, 6, 4, 2, 1, 9, 3, 7, 8]}, + {"clusterUnit": "market_2", "cycleIndex": "1", "seed": "-9223372036854775808", "armCount": 10, "expectedPermutation": [4, 2, 3, 8, 1, 5, 0, 9, 7, 6]}, + {"clusterUnit": "market_2", "cycleIndex": "2", "seed": "-9223372036854775808", "armCount": 10, "expectedPermutation": [7, 3, 2, 1, 5, 9, 8, 0, 4, 6]}, + {"clusterUnit": "market_2", "cycleIndex": "-1", "seed": "-9223372036854775808", "armCount": 10, "expectedPermutation": [4, 2, 5, 7, 9, 3, 8, 1, 0, 6]}, + {"clusterUnit": "market_2", "cycleIndex": "-2", "seed": "-9223372036854775808", "armCount": 10, "expectedPermutation": [5, 4, 1, 8, 9, 2, 7, 0, 6, 3]}, + {"clusterUnit": "market_2", "cycleIndex": "100", "seed": "-9223372036854775808", "armCount": 10, "expectedPermutation": [2, 9, 6, 4, 8, 0, 1, 5, 3, 7]}, + {"clusterUnit": "market_2", "cycleIndex": "-9223372036854775808", "seed": "-9223372036854775808", "armCount": 10, "expectedPermutation": [9, 5, 8, 2, 7, 3, 1, 6, 4, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "9223372036854775807", "seed": "-9223372036854775808", "armCount": 10, "expectedPermutation": [7, 9, 6, 0, 2, 5, 1, 8, 3, 4]}, + {"clusterUnit": "market_2", "cycleIndex": "0", "seed": "9223372036854775807", "armCount": 10, "expectedPermutation": [2, 0, 5, 9, 1, 7, 8, 3, 4, 6]}, + {"clusterUnit": "market_2", "cycleIndex": "1", "seed": "9223372036854775807", "armCount": 10, "expectedPermutation": [3, 7, 9, 8, 4, 2, 1, 6, 5, 0]}, + {"clusterUnit": "market_2", "cycleIndex": "2", "seed": "9223372036854775807", "armCount": 10, "expectedPermutation": [7, 6, 0, 2, 5, 9, 1, 3, 4, 8]}, + {"clusterUnit": "market_2", "cycleIndex": "-1", "seed": "9223372036854775807", "armCount": 10, "expectedPermutation": [9, 0, 6, 4, 3, 7, 5, 2, 8, 1]}, + {"clusterUnit": "market_2", "cycleIndex": "-2", "seed": "9223372036854775807", "armCount": 10, "expectedPermutation": [6, 1, 9, 4, 8, 5, 3, 2, 0, 7]}, + {"clusterUnit": "market_2", "cycleIndex": "100", "seed": "9223372036854775807", "armCount": 10, "expectedPermutation": [5, 9, 2, 1, 0, 4, 7, 6, 3, 8]}, + {"clusterUnit": "market_2", "cycleIndex": "-9223372036854775808", "seed": "9223372036854775807", "armCount": 10, "expectedPermutation": [0, 3, 7, 6, 4, 5, 2, 8, 1, 9]}, + {"clusterUnit": "market_2", "cycleIndex": "9223372036854775807", "seed": "9223372036854775807", "armCount": 10, "expectedPermutation": [8, 0, 5, 9, 7, 4, 6, 3, 2, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "0", "seed": "0", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "1", "seed": "0", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "2", "seed": "0", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "-1", "seed": "0", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "-2", "seed": "0", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "100", "seed": "0", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "-9223372036854775808", "seed": "0", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "9223372036854775807", "seed": "0", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "0", "seed": "1", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "1", "seed": "1", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "2", "seed": "1", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "-1", "seed": "1", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "-2", "seed": "1", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "100", "seed": "1", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "-9223372036854775808", "seed": "1", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "9223372036854775807", "seed": "1", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "0", "seed": "42", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "1", "seed": "42", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "2", "seed": "42", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "-1", "seed": "42", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "-2", "seed": "42", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "100", "seed": "42", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "-9223372036854775808", "seed": "42", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "9223372036854775807", "seed": "42", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "0", "seed": "-1", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "1", "seed": "-1", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "2", "seed": "-1", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "-1", "seed": "-1", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "-2", "seed": "-1", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "100", "seed": "-1", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "-9223372036854775808", "seed": "-1", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "9223372036854775807", "seed": "-1", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "0", "seed": "123456789", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "1", "seed": "123456789", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "2", "seed": "123456789", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "-1", "seed": "123456789", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "-2", "seed": "123456789", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "100", "seed": "123456789", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "-9223372036854775808", "seed": "123456789", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "9223372036854775807", "seed": "123456789", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "0", "seed": "-9223372036854775808", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "1", "seed": "-9223372036854775808", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "2", "seed": "-9223372036854775808", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "-1", "seed": "-9223372036854775808", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "-2", "seed": "-9223372036854775808", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "100", "seed": "-9223372036854775808", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "-9223372036854775808", "seed": "-9223372036854775808", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "9223372036854775807", "seed": "-9223372036854775808", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "0", "seed": "9223372036854775807", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "1", "seed": "9223372036854775807", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "2", "seed": "9223372036854775807", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "-1", "seed": "9223372036854775807", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "-2", "seed": "9223372036854775807", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "100", "seed": "9223372036854775807", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "-9223372036854775808", "seed": "9223372036854775807", "armCount": 2, "expectedPermutation": [1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "9223372036854775807", "seed": "9223372036854775807", "armCount": 2, "expectedPermutation": [0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "0", "seed": "0", "armCount": 3, "expectedPermutation": [2, 0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "1", "seed": "0", "armCount": 3, "expectedPermutation": [1, 0, 2]}, + {"clusterUnit": 123456789, "cycleIndex": "2", "seed": "0", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": 123456789, "cycleIndex": "-1", "seed": "0", "armCount": 3, "expectedPermutation": [2, 0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "-2", "seed": "0", "armCount": 3, "expectedPermutation": [0, 2, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "100", "seed": "0", "armCount": 3, "expectedPermutation": [2, 0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "-9223372036854775808", "seed": "0", "armCount": 3, "expectedPermutation": [0, 2, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "9223372036854775807", "seed": "0", "armCount": 3, "expectedPermutation": [0, 2, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "0", "seed": "1", "armCount": 3, "expectedPermutation": [2, 0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "1", "seed": "1", "armCount": 3, "expectedPermutation": [2, 1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "2", "seed": "1", "armCount": 3, "expectedPermutation": [2, 0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "-1", "seed": "1", "armCount": 3, "expectedPermutation": [2, 0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "-2", "seed": "1", "armCount": 3, "expectedPermutation": [1, 0, 2]}, + {"clusterUnit": 123456789, "cycleIndex": "100", "seed": "1", "armCount": 3, "expectedPermutation": [0, 2, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "-9223372036854775808", "seed": "1", "armCount": 3, "expectedPermutation": [1, 0, 2]}, + {"clusterUnit": 123456789, "cycleIndex": "9223372036854775807", "seed": "1", "armCount": 3, "expectedPermutation": [1, 0, 2]}, + {"clusterUnit": 123456789, "cycleIndex": "0", "seed": "42", "armCount": 3, "expectedPermutation": [2, 1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "1", "seed": "42", "armCount": 3, "expectedPermutation": [2, 1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "2", "seed": "42", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": 123456789, "cycleIndex": "-1", "seed": "42", "armCount": 3, "expectedPermutation": [2, 0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "-2", "seed": "42", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": 123456789, "cycleIndex": "100", "seed": "42", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": 123456789, "cycleIndex": "-9223372036854775808", "seed": "42", "armCount": 3, "expectedPermutation": [1, 2, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "9223372036854775807", "seed": "42", "armCount": 3, "expectedPermutation": [2, 1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "0", "seed": "-1", "armCount": 3, "expectedPermutation": [1, 2, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "1", "seed": "-1", "armCount": 3, "expectedPermutation": [2, 0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "2", "seed": "-1", "armCount": 3, "expectedPermutation": [2, 0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "-1", "seed": "-1", "armCount": 3, "expectedPermutation": [1, 0, 2]}, + {"clusterUnit": 123456789, "cycleIndex": "-2", "seed": "-1", "armCount": 3, "expectedPermutation": [1, 2, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "100", "seed": "-1", "armCount": 3, "expectedPermutation": [2, 1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "-9223372036854775808", "seed": "-1", "armCount": 3, "expectedPermutation": [1, 2, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "9223372036854775807", "seed": "-1", "armCount": 3, "expectedPermutation": [0, 2, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "0", "seed": "123456789", "armCount": 3, "expectedPermutation": [1, 0, 2]}, + {"clusterUnit": 123456789, "cycleIndex": "1", "seed": "123456789", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": 123456789, "cycleIndex": "2", "seed": "123456789", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": 123456789, "cycleIndex": "-1", "seed": "123456789", "armCount": 3, "expectedPermutation": [0, 2, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "-2", "seed": "123456789", "armCount": 3, "expectedPermutation": [1, 0, 2]}, + {"clusterUnit": 123456789, "cycleIndex": "100", "seed": "123456789", "armCount": 3, "expectedPermutation": [1, 0, 2]}, + {"clusterUnit": 123456789, "cycleIndex": "-9223372036854775808", "seed": "123456789", "armCount": 3, "expectedPermutation": [2, 1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "9223372036854775807", "seed": "123456789", "armCount": 3, "expectedPermutation": [1, 2, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "0", "seed": "-9223372036854775808", "armCount": 3, "expectedPermutation": [1, 0, 2]}, + {"clusterUnit": 123456789, "cycleIndex": "1", "seed": "-9223372036854775808", "armCount": 3, "expectedPermutation": [0, 2, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "2", "seed": "-9223372036854775808", "armCount": 3, "expectedPermutation": [1, 0, 2]}, + {"clusterUnit": 123456789, "cycleIndex": "-1", "seed": "-9223372036854775808", "armCount": 3, "expectedPermutation": [2, 1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "-2", "seed": "-9223372036854775808", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": 123456789, "cycleIndex": "100", "seed": "-9223372036854775808", "armCount": 3, "expectedPermutation": [0, 1, 2]}, + {"clusterUnit": 123456789, "cycleIndex": "-9223372036854775808", "seed": "-9223372036854775808", "armCount": 3, "expectedPermutation": [2, 1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "9223372036854775807", "seed": "-9223372036854775808", "armCount": 3, "expectedPermutation": [0, 2, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "0", "seed": "9223372036854775807", "armCount": 3, "expectedPermutation": [0, 2, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "1", "seed": "9223372036854775807", "armCount": 3, "expectedPermutation": [2, 1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "2", "seed": "9223372036854775807", "armCount": 3, "expectedPermutation": [2, 0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "-1", "seed": "9223372036854775807", "armCount": 3, "expectedPermutation": [0, 2, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "-2", "seed": "9223372036854775807", "armCount": 3, "expectedPermutation": [1, 0, 2]}, + {"clusterUnit": 123456789, "cycleIndex": "100", "seed": "9223372036854775807", "armCount": 3, "expectedPermutation": [2, 0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "-9223372036854775808", "seed": "9223372036854775807", "armCount": 3, "expectedPermutation": [2, 0, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "9223372036854775807", "seed": "9223372036854775807", "armCount": 3, "expectedPermutation": [2, 1, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "0", "seed": "0", "armCount": 10, "expectedPermutation": [9, 0, 1, 7, 3, 4, 5, 8, 2, 6]}, + {"clusterUnit": 123456789, "cycleIndex": "1", "seed": "0", "armCount": 10, "expectedPermutation": [4, 9, 7, 6, 1, 5, 8, 2, 0, 3]}, + {"clusterUnit": 123456789, "cycleIndex": "2", "seed": "0", "armCount": 10, "expectedPermutation": [4, 1, 5, 8, 9, 6, 2, 3, 0, 7]}, + {"clusterUnit": 123456789, "cycleIndex": "-1", "seed": "0", "armCount": 10, "expectedPermutation": [5, 0, 1, 6, 7, 3, 9, 8, 2, 4]}, + {"clusterUnit": 123456789, "cycleIndex": "-2", "seed": "0", "armCount": 10, "expectedPermutation": [5, 2, 9, 4, 1, 0, 8, 6, 3, 7]}, + {"clusterUnit": 123456789, "cycleIndex": "100", "seed": "0", "armCount": 10, "expectedPermutation": [7, 5, 8, 3, 0, 9, 2, 6, 1, 4]}, + {"clusterUnit": 123456789, "cycleIndex": "-9223372036854775808", "seed": "0", "armCount": 10, "expectedPermutation": [0, 5, 7, 9, 3, 2, 4, 1, 8, 6]}, + {"clusterUnit": 123456789, "cycleIndex": "9223372036854775807", "seed": "0", "armCount": 10, "expectedPermutation": [9, 5, 6, 3, 1, 2, 8, 7, 4, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "0", "seed": "1", "armCount": 10, "expectedPermutation": [9, 7, 4, 8, 1, 0, 5, 3, 6, 2]}, + {"clusterUnit": 123456789, "cycleIndex": "1", "seed": "1", "armCount": 10, "expectedPermutation": [9, 3, 7, 1, 8, 5, 0, 4, 6, 2]}, + {"clusterUnit": 123456789, "cycleIndex": "2", "seed": "1", "armCount": 10, "expectedPermutation": [9, 5, 1, 8, 0, 3, 4, 6, 7, 2]}, + {"clusterUnit": 123456789, "cycleIndex": "-1", "seed": "1", "armCount": 10, "expectedPermutation": [4, 9, 3, 1, 6, 8, 7, 2, 5, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "-2", "seed": "1", "armCount": 10, "expectedPermutation": [1, 0, 6, 9, 8, 5, 2, 7, 3, 4]}, + {"clusterUnit": 123456789, "cycleIndex": "100", "seed": "1", "armCount": 10, "expectedPermutation": [0, 7, 8, 3, 1, 5, 4, 9, 6, 2]}, + {"clusterUnit": 123456789, "cycleIndex": "-9223372036854775808", "seed": "1", "armCount": 10, "expectedPermutation": [1, 9, 5, 4, 8, 2, 0, 7, 6, 3]}, + {"clusterUnit": 123456789, "cycleIndex": "9223372036854775807", "seed": "1", "armCount": 10, "expectedPermutation": [3, 4, 2, 9, 0, 5, 1, 7, 6, 8]}, + {"clusterUnit": 123456789, "cycleIndex": "0", "seed": "42", "armCount": 10, "expectedPermutation": [6, 1, 9, 7, 5, 8, 3, 4, 2, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "1", "seed": "42", "armCount": 10, "expectedPermutation": [2, 8, 3, 0, 1, 7, 9, 6, 4, 5]}, + {"clusterUnit": 123456789, "cycleIndex": "2", "seed": "42", "armCount": 10, "expectedPermutation": [5, 1, 3, 2, 0, 6, 9, 8, 7, 4]}, + {"clusterUnit": 123456789, "cycleIndex": "-1", "seed": "42", "armCount": 10, "expectedPermutation": [2, 7, 6, 9, 5, 0, 1, 8, 3, 4]}, + {"clusterUnit": 123456789, "cycleIndex": "-2", "seed": "42", "armCount": 10, "expectedPermutation": [8, 5, 4, 3, 2, 1, 0, 7, 9, 6]}, + {"clusterUnit": 123456789, "cycleIndex": "100", "seed": "42", "armCount": 10, "expectedPermutation": [4, 6, 5, 9, 0, 2, 7, 3, 8, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "-9223372036854775808", "seed": "42", "armCount": 10, "expectedPermutation": [1, 9, 4, 0, 6, 8, 7, 3, 2, 5]}, + {"clusterUnit": 123456789, "cycleIndex": "9223372036854775807", "seed": "42", "armCount": 10, "expectedPermutation": [8, 1, 4, 7, 0, 9, 3, 2, 6, 5]}, + {"clusterUnit": 123456789, "cycleIndex": "0", "seed": "-1", "armCount": 10, "expectedPermutation": [8, 4, 5, 2, 3, 0, 9, 7, 6, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "1", "seed": "-1", "armCount": 10, "expectedPermutation": [8, 0, 4, 2, 6, 7, 9, 1, 3, 5]}, + {"clusterUnit": 123456789, "cycleIndex": "2", "seed": "-1", "armCount": 10, "expectedPermutation": [2, 8, 7, 6, 9, 0, 5, 4, 3, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "-1", "seed": "-1", "armCount": 10, "expectedPermutation": [7, 3, 4, 8, 5, 6, 9, 1, 0, 2]}, + {"clusterUnit": 123456789, "cycleIndex": "-2", "seed": "-1", "armCount": 10, "expectedPermutation": [5, 6, 0, 1, 2, 8, 4, 3, 7, 9]}, + {"clusterUnit": 123456789, "cycleIndex": "100", "seed": "-1", "armCount": 10, "expectedPermutation": [9, 6, 3, 4, 8, 2, 1, 7, 0, 5]}, + {"clusterUnit": 123456789, "cycleIndex": "-9223372036854775808", "seed": "-1", "armCount": 10, "expectedPermutation": [4, 2, 0, 5, 1, 3, 8, 9, 6, 7]}, + {"clusterUnit": 123456789, "cycleIndex": "9223372036854775807", "seed": "-1", "armCount": 10, "expectedPermutation": [0, 4, 1, 6, 3, 5, 9, 2, 8, 7]}, + {"clusterUnit": 123456789, "cycleIndex": "0", "seed": "123456789", "armCount": 10, "expectedPermutation": [9, 0, 2, 7, 5, 4, 3, 1, 8, 6]}, + {"clusterUnit": 123456789, "cycleIndex": "1", "seed": "123456789", "armCount": 10, "expectedPermutation": [6, 1, 2, 7, 5, 3, 0, 9, 4, 8]}, + {"clusterUnit": 123456789, "cycleIndex": "2", "seed": "123456789", "armCount": 10, "expectedPermutation": [7, 9, 2, 8, 5, 6, 0, 4, 3, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "-1", "seed": "123456789", "armCount": 10, "expectedPermutation": [5, 2, 8, 4, 0, 9, 1, 7, 6, 3]}, + {"clusterUnit": 123456789, "cycleIndex": "-2", "seed": "123456789", "armCount": 10, "expectedPermutation": [1, 0, 7, 3, 5, 6, 4, 8, 9, 2]}, + {"clusterUnit": 123456789, "cycleIndex": "100", "seed": "123456789", "armCount": 10, "expectedPermutation": [3, 0, 7, 4, 1, 6, 5, 2, 8, 9]}, + {"clusterUnit": 123456789, "cycleIndex": "-9223372036854775808", "seed": "123456789", "armCount": 10, "expectedPermutation": [9, 1, 3, 5, 6, 4, 7, 0, 8, 2]}, + {"clusterUnit": 123456789, "cycleIndex": "9223372036854775807", "seed": "123456789", "armCount": 10, "expectedPermutation": [6, 2, 0, 7, 8, 1, 3, 4, 5, 9]}, + {"clusterUnit": 123456789, "cycleIndex": "0", "seed": "-9223372036854775808", "armCount": 10, "expectedPermutation": [9, 0, 8, 2, 7, 5, 3, 6, 4, 1]}, + {"clusterUnit": 123456789, "cycleIndex": "1", "seed": "-9223372036854775808", "armCount": 10, "expectedPermutation": [0, 5, 1, 8, 2, 3, 9, 6, 7, 4]}, + {"clusterUnit": 123456789, "cycleIndex": "2", "seed": "-9223372036854775808", "armCount": 10, "expectedPermutation": [5, 0, 8, 2, 9, 4, 1, 6, 3, 7]}, + {"clusterUnit": 123456789, "cycleIndex": "-1", "seed": "-9223372036854775808", "armCount": 10, "expectedPermutation": [5, 4, 8, 6, 1, 9, 7, 2, 0, 3]}, + {"clusterUnit": 123456789, "cycleIndex": "-2", "seed": "-9223372036854775808", "armCount": 10, "expectedPermutation": [0, 1, 3, 7, 6, 4, 2, 9, 8, 5]}, + {"clusterUnit": 123456789, "cycleIndex": "100", "seed": "-9223372036854775808", "armCount": 10, "expectedPermutation": [0, 5, 2, 9, 6, 1, 4, 8, 3, 7]}, + {"clusterUnit": 123456789, "cycleIndex": "-9223372036854775808", "seed": "-9223372036854775808", "armCount": 10, "expectedPermutation": [9, 8, 5, 1, 3, 0, 6, 4, 7, 2]}, + {"clusterUnit": 123456789, "cycleIndex": "9223372036854775807", "seed": "-9223372036854775808", "armCount": 10, "expectedPermutation": [8, 3, 7, 2, 0, 1, 4, 5, 9, 6]}, + {"clusterUnit": 123456789, "cycleIndex": "0", "seed": "9223372036854775807", "armCount": 10, "expectedPermutation": [5, 6, 8, 1, 7, 9, 4, 2, 3, 0]}, + {"clusterUnit": 123456789, "cycleIndex": "1", "seed": "9223372036854775807", "armCount": 10, "expectedPermutation": [4, 8, 0, 6, 9, 7, 5, 2, 1, 3]}, + {"clusterUnit": 123456789, "cycleIndex": "2", "seed": "9223372036854775807", "armCount": 10, "expectedPermutation": [2, 0, 8, 6, 3, 7, 9, 5, 1, 4]}, + {"clusterUnit": 123456789, "cycleIndex": "-1", "seed": "9223372036854775807", "armCount": 10, "expectedPermutation": [6, 2, 3, 5, 7, 1, 9, 8, 0, 4]}, + {"clusterUnit": 123456789, "cycleIndex": "-2", "seed": "9223372036854775807", "armCount": 10, "expectedPermutation": [5, 6, 7, 4, 2, 1, 8, 3, 0, 9]}, + {"clusterUnit": 123456789, "cycleIndex": "100", "seed": "9223372036854775807", "armCount": 10, "expectedPermutation": [4, 9, 1, 7, 2, 5, 0, 6, 3, 8]}, + {"clusterUnit": 123456789, "cycleIndex": "-9223372036854775808", "seed": "9223372036854775807", "armCount": 10, "expectedPermutation": [3, 4, 1, 2, 8, 0, 5, 9, 6, 7]}, + {"clusterUnit": 123456789, "cycleIndex": "9223372036854775807", "seed": "9223372036854775807", "armCount": 10, "expectedPermutation": [2, 5, 4, 6, 7, 9, 1, 0, 3, 8]} + ] +} diff --git a/core-api/src/test/resources/switchback_context.json b/core-api/src/test/resources/switchback_context.json new file mode 100644 index 0000000..53dca93 --- /dev/null +++ b/core-api/src/test/resources/switchback_context.json @@ -0,0 +1,88 @@ +{ + "experiments":[ + { + "id":1, + "name":"exp_test_switchback", + "iteration":1, + "unitType":"store_id", + "seedHi":3603515, + "seedLo":233373850, + "split":[ + 0.5, + 0.5 + ], + "trafficSeedHi":449867249, + "trafficSeedLo":455443629, + "trafficSplit":[ + 0.0, + 1.0 + ], + "fullOnVariant":0, + "applications":[ + { + "name":"website" + } + ], + "variants":[ + { + "name":"A", + "config":null + }, + { + "name":"B", + "config":"{\"banner.border\":1}" + } + ], + "audience":null, + "randomizationType":"switchback", + "switchbackClusterUnitType":"store_id", + "switchbackStartAt":1767225600000, + "switchbackPeriodMs":3600000, + "switchbackCarryoverDurationMs":900000, + "switchbackBurnInDurationMs":600000, + "switchbackBurnOutDurationMs":0, + "switchbackDesignType":"balanced_block", + "switchbackEstimand":"global_average_treatment_effect", + "switchbackClusterCount":50, + "switchbackClusterAttribute":"geo/city", + "switchbackFutureField":{"nested":[1,2,3]} + }, + { + "id":2, + "name":"exp_test_unit", + "iteration":1, + "unitType":"session_id", + "seedHi":55006150, + "seedLo":47189152, + "split":[ + 0.5, + 0.5 + ], + "trafficSeedHi":705671872, + "trafficSeedLo":212903484, + "trafficSplit":[ + 0.0, + 1.0 + ], + "fullOnVariant":0, + "applications":[ + { + "name":"website" + } + ], + "variants":[ + { + "name":"A", + "config":null + }, + { + "name":"B", + "config":null + } + ], + "audience":null, + "randomizationType":"unit", + "switchbackStartAt":null + } + ] +}