@@ -28,7 +28,7 @@ import {
2828} from '../../../src/remote-config/remote-config-api' ;
2929import { v4 as uuidv4 } from 'uuid' ;
3030import { clone } from 'lodash' ;
31- import * as farmhash from 'farmhash-modern ' ;
31+ import * as crypto from 'crypto ' ;
3232
3333const expect = chai . expect ;
3434
@@ -251,9 +251,8 @@ describe('ConditionEvaluator', () => {
251251
252252 it ( 'should use zero for undefined microPercent' , ( ) => {
253253 // Stubs ID hasher to return a number larger than zero.
254- const stub = sinon
255- . stub ( farmhash , 'fingerprint64' )
256- . returns ( 1n ) ;
254+ const stub = sinon . stub ( crypto . Hash . prototype , 'digest' ) ;
255+ stub . withArgs ( 'hex' ) . returns ( '1' ) ;
257256 stubs . push ( stub ) ;
258257
259258 const condition = {
@@ -284,9 +283,8 @@ describe('ConditionEvaluator', () => {
284283
285284 it ( 'should use zeros for undefined microPercentRange' , ( ) => {
286285 // Stubs ID hasher to return a number in range.
287- const stub = sinon
288- . stub ( farmhash , 'fingerprint64' )
289- . returns ( 1n ) ;
286+ const stub = sinon . stub ( crypto . Hash . prototype , 'digest' ) ;
287+ stub . withArgs ( 'hex' ) . returns ( '1' ) ;
290288 stubs . push ( stub ) ;
291289
292290 const condition = {
@@ -317,9 +315,8 @@ describe('ConditionEvaluator', () => {
317315
318316 it ( 'should use zero for undefined microPercentUpperBound' , ( ) => {
319317 // Stubs ID hasher to return a number outside range.
320- const stub = sinon
321- . stub ( farmhash , 'fingerprint64' )
322- . returns ( 1n ) ;
318+ const stub = sinon . stub ( crypto . Hash . prototype , 'digest' ) ;
319+ stub . withArgs ( 'hex' ) . returns ( '1' ) ;
323320 stubs . push ( stub ) ;
324321
325322 const condition = {
@@ -353,9 +350,8 @@ describe('ConditionEvaluator', () => {
353350
354351 it ( 'should use zero for undefined microPercentLowerBound' , ( ) => {
355352 // Stubs ID hasher to return a number in range.
356- const stub = sinon
357- . stub ( farmhash , 'fingerprint64' )
358- . returns ( 1n ) ;
353+ const stub = sinon . stub ( crypto . Hash . prototype , 'digest' ) ;
354+ stub . withArgs ( 'hex' ) . returns ( '1' ) ;
359355 stubs . push ( stub ) ;
360356
361357 const condition = {
@@ -388,9 +384,9 @@ describe('ConditionEvaluator', () => {
388384 } ) ;
389385
390386 it ( 'should evaluate 9 as less or equal to 10' , ( ) => {
391- const stub = sinon
392- . stub ( farmhash , 'fingerprint64' )
393- . returns ( 9n ) ;
387+ const stub = sinon . stub ( crypto . Hash . prototype , 'digest' ) ;
388+ stub . withArgs ( 'hex' ) . returns ( '9' ) ;
389+ stubs . push ( stub ) ;
394390
395391 stubs . push ( stub ) ;
396392 const condition = {
@@ -419,9 +415,9 @@ describe('ConditionEvaluator', () => {
419415 } ) ;
420416
421417 it ( 'should evaluate 10 as less or equal to 10' , ( ) => {
422- const stub = sinon
423- . stub ( farmhash , 'fingerprint64' )
424- . returns ( 10n ) ;
418+ const stub = sinon . stub ( crypto . Hash . prototype , 'digest' ) ;
419+ stub . withArgs ( 'hex' ) . returns ( ( 10 ) . toString ( 16 ) ) ;
420+ stubs . push ( stub ) ;
425421
426422 stubs . push ( stub ) ;
427423 const condition = {
@@ -450,9 +446,9 @@ describe('ConditionEvaluator', () => {
450446 } ) ;
451447
452448 it ( 'should evaluate 11 as not less or equal to 10' , ( ) => {
453- const stub = sinon
454- . stub ( farmhash , 'fingerprint64' )
455- . returns ( 11n ) ;
449+ const stub = sinon . stub ( crypto . Hash . prototype , 'digest' ) ;
450+ stub . withArgs ( 'hex' ) . returns ( ( 11 ) . toString ( 16 ) ) ;
451+ stubs . push ( stub ) ;
456452
457453 stubs . push ( stub ) ;
458454 const condition = {
@@ -481,9 +477,9 @@ describe('ConditionEvaluator', () => {
481477 } ) ;
482478
483479 it ( 'should negate -11 to 11 and evaluate as not less or equal to 10' , ( ) => {
484- const stub = sinon
485- . stub ( farmhash , 'fingerprint64' )
486- . returns ( - 11n ) ;
480+ const stub = sinon . stub ( crypto . Hash . prototype , 'digest' ) ;
481+ stub . withArgs ( 'hex' ) . returns ( ( 11 ) . toString ( 16 ) ) ;
482+ stubs . push ( stub ) ;
487483
488484 stubs . push ( stub ) ;
489485 const condition = {
@@ -540,9 +536,9 @@ describe('ConditionEvaluator', () => {
540536 } ) ;
541537
542538 it ( 'should evaluate 11M as greater than 10M' , ( ) => {
543- const stub = sinon
544- . stub ( farmhash , 'fingerprint64' )
545- . returns ( 11n ) ;
539+ const stub = sinon . stub ( crypto . Hash . prototype , 'digest' ) ;
540+ stub . withArgs ( 'hex' ) . returns ( ( 11 ) . toString ( 16 ) ) ;
541+ stubs . push ( stub ) ;
546542
547543 stubs . push ( stub ) ;
548544 const condition = {
@@ -571,9 +567,8 @@ describe('ConditionEvaluator', () => {
571567 } ) ;
572568
573569 it ( 'should evaluate 9 as not greater than 10' , ( ) => {
574- const stub = sinon
575- . stub ( farmhash , 'fingerprint64' )
576- . returns ( 9n ) ;
570+ const stub = sinon . stub ( crypto . Hash . prototype , 'digest' ) ;
571+ stub . withArgs ( 'hex' ) . returns ( '9' ) ;
577572 stubs . push ( stub ) ;
578573
579574 const condition = {
@@ -661,9 +656,8 @@ describe('ConditionEvaluator', () => {
661656 } ) ;
662657
663658 it ( 'should evaluate 10 as between 9 and 11' , ( ) => {
664- const stub = sinon
665- . stub ( farmhash , 'fingerprint64' )
666- . returns ( 10n ) ;
659+ const stub = sinon . stub ( crypto . Hash . prototype , 'digest' ) ;
660+ stub . withArgs ( 'hex' ) . returns ( ( 10 ) . toString ( 16 ) ) ;
667661 stubs . push ( stub ) ;
668662
669663 const condition = {
@@ -726,9 +720,8 @@ describe('ConditionEvaluator', () => {
726720 } ) ;
727721
728722 it ( 'should evaluate 12 as not between 9 and 11' , ( ) => {
729- const stub = sinon
730- . stub ( farmhash , 'fingerprint64' )
731- . returns ( 12n ) ;
723+ const stub = sinon . stub ( crypto . Hash . prototype , 'digest' ) ;
724+ stub . withArgs ( 'hex' ) . returns ( ( 12 ) . toString ( 16 ) ) ;
732725 stubs . push ( stub ) ;
733726
734727 const condition = {
@@ -759,6 +752,51 @@ describe('ConditionEvaluator', () => {
759752 expect ( actual ) . to . be . false ;
760753 } ) ;
761754
755+ describe ( 'known percent condition values' , ( ) => {
756+ // This test is useful for ensuring consistency across all places we
757+ // evaluate percent conditions. It creates a set of 10 conditions targeting 50%
758+ // with randomizationIds 0-9 and a constant `seed` value.
759+ const conditionEvaluator = new ConditionEvaluator ( ) ;
760+
761+ const percentCondition = {
762+ percentOperator : PercentConditionOperator . BETWEEN ,
763+ microPercentRange : {
764+ microPercentLowerBound : 0 ,
765+ microPercentUpperBound : 50_000_000 // 50%
766+ } ,
767+ } ;
768+
769+ const testCases = [
770+ { seed : '1' , randomizationId : 'one' , result : false } ,
771+ { seed : '2' , randomizationId : 'two' , result : false } ,
772+ { seed : '3' , randomizationId : 'three' , result : true } ,
773+ { seed : '4' , randomizationId : 'four' , result : false } ,
774+ { seed : '5' , randomizationId : 'five' , result : true } ,
775+ { seed : '' , randomizationId : '😊' , result : true } ,
776+ { seed : '' , randomizationId : '😀' , result : false } ,
777+ { seed : 'hêl£o' , randomizationId : 'wørlÐ' , result : false } ,
778+ { seed : 'řemøťe' , randomizationId : 'çōnfįġ' , result : true } ,
779+ { seed : 'long' , randomizationId : '.' . repeat ( 100 ) , result : true } ,
780+ { seed : 'very-long' , randomizationId : '.' . repeat ( 1000 ) , result : false } ,
781+ ] ;
782+
783+ testCases . map ( ( { randomizationId, seed, result } ) => {
784+
785+ const idSummary = randomizationId . length > 25
786+ ? `a ${ randomizationId . length } character randomizationID`
787+ : `"${ randomizationId } "` ;
788+
789+ it ( `should evaluate ${ idSummary } with seed "${ seed } " to ${ result } ` , ( ) => {
790+ const context = { randomizationId } ;
791+ const evalResult = conditionEvaluator . evaluateConditions ( [ {
792+ name : 'is_enabled' ,
793+ condition : { percent : { ...percentCondition , seed } }
794+ } ] , context ) ;
795+ expect ( evalResult . get ( 'is_enabled' ) ) . to . equal ( result ) ;
796+ } ) ;
797+ } ) ;
798+ } ) ;
799+
762800 // The following tests are probabilistic. They use tolerances based on
763801 // standard deviations to balance accuracy and flakiness. Random IDs will
764802 // hash to the target range + 3 standard deviations 99.7% of the time,
@@ -958,15 +996,15 @@ describe('ConditionEvaluator', () => {
958996 describe ( 'STRING_CONTAINS' , ( ) => {
959997 const testCases : CustomSignalTestCase [ ] = [
960998 { targets : [ 'foo' , 'biz' ] , actual : 'foobar' , outcome : true } ,
961- { targets : [ 'foo' , 'biz' ] , actual : 'bar' , outcome : false } ,
999+ { targets : [ 'foo' , 'biz' ] , actual : 'bar' , outcome : false } ,
9621000 ] ;
9631001
9641002 testCases . forEach ( runCustomSignalTestCase ( CustomSignalOperator . STRING_CONTAINS ) ) ;
9651003 } ) ;
9661004
9671005 describe ( 'STRING_DOES_NOT_CONTAIN' , ( ) => {
9681006 const testCases : CustomSignalTestCase [ ] = [
969- { targets : [ 'foo' , 'biz' ] , actual : 'bar' , outcome : true } ,
1007+ { targets : [ 'foo' , 'biz' ] , actual : 'bar' , outcome : true } ,
9701008 { targets : [ 'foo' , 'biz' ] , actual : 'foobar' , outcome : false } ,
9711009 ] ;
9721010
@@ -1135,77 +1173,4 @@ describe('ConditionEvaluator', () => {
11351173 } ) ;
11361174 } ) ;
11371175 } ) ;
1138-
1139- describe ( 'hashSeededRandomizationId' , ( ) => {
1140- // The Farmhash algorithm produces a 64 bit unsigned integer,
1141- // which we convert to a signed integer for legacy compatibility.
1142- // This has caused confusion in the past, so we explicitly
1143- // test here.
1144- it ( 'should leave numbers <= 2^63-1 (max signed long) as is' , function ( ) {
1145- if ( nodeVersion . startsWith ( '14' ) ) {
1146- this . skip ( ) ;
1147- }
1148-
1149- const stub = sinon
1150- . stub ( farmhash , 'fingerprint64' )
1151- // 2^63-1 = 9223372036854775807.
1152- . returns ( BigInt ( '9223372036854775807' ) ) ;
1153- stubs . push ( stub ) ;
1154-
1155- const actual = ConditionEvaluator . hashSeededRandomizationId ( 'anything' ) ;
1156-
1157- expect ( actual ) . to . equal ( BigInt ( '9223372036854775807' ) )
1158- } ) ;
1159-
1160- it ( 'should convert 2^63 to negative (min signed long) and then find the absolute value' , function ( ) {
1161- if ( nodeVersion . startsWith ( '14' ) ) {
1162- this . skip ( ) ;
1163- }
1164-
1165- const stub = sinon
1166- . stub ( farmhash , 'fingerprint64' )
1167- // 2^63 = 9223372036854775808.
1168- . returns ( BigInt ( '9223372036854775808' ) ) ;
1169- stubs . push ( stub ) ;
1170-
1171- const actual = ConditionEvaluator . hashSeededRandomizationId ( 'anything' ) ;
1172-
1173- // 2^63 is the negation of 2^63-1
1174- expect ( actual ) . to . equal ( BigInt ( '9223372036854775808' ) )
1175- } ) ;
1176-
1177- it ( 'should convert 2^63+1 to negative and then find the absolute value' , function ( ) {
1178- if ( nodeVersion . startsWith ( '14' ) ) {
1179- this . skip ( ) ;
1180- }
1181-
1182- const stub = sinon
1183- . stub ( farmhash , 'fingerprint64' )
1184- // 2^63+1 9223372036854775809.
1185- . returns ( BigInt ( '9223372036854775809' ) ) ;
1186- stubs . push ( stub ) ;
1187-
1188- const actual = ConditionEvaluator . hashSeededRandomizationId ( 'anything' ) ;
1189-
1190- // 2^63+1 is larger than 2^63, so the absolute value is smaller
1191- expect ( actual ) . to . equal ( BigInt ( '9223372036854775807' ) )
1192- } ) ;
1193-
1194- it ( 'should handle the value that initially caused confusion' , function ( ) {
1195- if ( nodeVersion . startsWith ( '14' ) ) {
1196- this . skip ( ) ;
1197- }
1198-
1199- const stub = sinon
1200- . stub ( farmhash , 'fingerprint64' )
1201- // We were initially confused about the nature of this value ...
1202- . returns ( BigInt ( '16081085603393958147' ) ) ;
1203- stubs . push ( stub ) ;
1204-
1205- const actual = ConditionEvaluator . hashSeededRandomizationId ( 'anything' ) ;
1206-
1207- // ... Now we know it's the unsigned equivalent of this absolute value.
1208- expect ( actual ) . to . equal ( BigInt ( '2365658470315593469' ) )
1209- } ) ;
1210- } ) ;
12111176} ) ;
0 commit comments