mirror of
https://github.com/hanzoai/firebase-js-sdk.git
synced 2026-09-24 16:59:10 +00:00
280 lines
7.9 KiB
TypeScript
280 lines
7.9 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2017 Google LLC
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
import { expect } from 'chai';
|
|
|
|
import { debugCast } from '../../../src/util/assert';
|
|
import { compareUtf8Strings, immediateSuccessor } from '../../../src/util/misc';
|
|
import { mask } from '../../util/helpers';
|
|
|
|
describe('immediateSuccessor', () => {
|
|
it('generates the correct immediate successors', () => {
|
|
expect(immediateSuccessor('hello')).to.equal('hello\0');
|
|
expect(immediateSuccessor('')).to.equal('\0');
|
|
});
|
|
});
|
|
|
|
describe('typeCast', () => {
|
|
it('can cast types', () => {
|
|
class Foo {}
|
|
class Bar extends Foo {}
|
|
const foo: Foo = new Bar();
|
|
const _: Bar = debugCast(foo, Bar);
|
|
});
|
|
|
|
it('validates types', () => {
|
|
class Foo {}
|
|
class Bar {}
|
|
const foo = new Foo();
|
|
expect(() => debugCast(foo, Bar)).to.throw(
|
|
"Expected type 'Bar', but was 'Foo'"
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('FieldMask', () => {
|
|
it('cannot contain duplicate fields', () => {
|
|
expect(() => mask('a', 'b', 'a')).to.throw(
|
|
'FieldMask contains field that is not unique: a'
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('CompareUtf8Strings', () => {
|
|
it('compareUtf8Strings should return correct results', () => {
|
|
const errors = [];
|
|
const seed = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
|
|
let passCount = 0;
|
|
const stringGenerator = new StringGenerator(new Random(seed), 0.33, 20);
|
|
const stringPairGenerator = new StringPairGenerator(stringGenerator);
|
|
|
|
for (let i = 0; i < 1000000 && errors.length < 10; i++) {
|
|
const { s1, s2 } = stringPairGenerator.next();
|
|
|
|
const actual = compareUtf8Strings(s1, s2);
|
|
const expected = Buffer.from(s1, 'utf8').compare(Buffer.from(s2, 'utf8'));
|
|
|
|
if (actual === expected) {
|
|
passCount++;
|
|
} else {
|
|
errors.push(
|
|
`compareUtf8Strings(s1="${s1}", s2="${s2}") returned ${actual}, ` +
|
|
`but expected ${expected} (i=${i}, s1.length=${s1.length}, s2.length=${s2.length})`
|
|
);
|
|
}
|
|
}
|
|
|
|
if (errors.length > 0) {
|
|
console.error(
|
|
`${errors.length} test cases failed, ${passCount} test cases passed, seed=${seed};`
|
|
);
|
|
errors.forEach((error, index) =>
|
|
console.error(`errors[${index}]: ${error}`)
|
|
);
|
|
throw new Error('Test failed');
|
|
}
|
|
}).timeout(20000);
|
|
|
|
class StringPair {
|
|
constructor(readonly s1: string, readonly s2: string) {}
|
|
}
|
|
|
|
class StringPairGenerator {
|
|
constructor(private stringGenerator: StringGenerator) {}
|
|
|
|
next(): StringPair {
|
|
const prefix = this.stringGenerator.next();
|
|
const s1 = prefix + this.stringGenerator.next();
|
|
const s2 = prefix + this.stringGenerator.next();
|
|
return new StringPair(s1, s2);
|
|
}
|
|
}
|
|
|
|
class StringGenerator {
|
|
private static readonly DEFAULT_SURROGATE_PAIR_PROBABILITY = 0.33;
|
|
private static readonly DEFAULT_MAX_LENGTH = 20;
|
|
|
|
// Pseudo-random number generator. Seed can be set for repeatable tests.
|
|
private readonly rnd: Random;
|
|
private readonly surrogatePairProbability: number;
|
|
private readonly maxLength: number;
|
|
|
|
constructor(seed: number);
|
|
constructor(
|
|
rnd: Random,
|
|
surrogatePairProbability: number,
|
|
maxLength: number
|
|
);
|
|
constructor(
|
|
seedOrRnd: number | Random,
|
|
surrogatePairProbability?: number,
|
|
maxLength?: number
|
|
) {
|
|
if (typeof seedOrRnd === 'number') {
|
|
this.rnd = new Random(seedOrRnd);
|
|
this.surrogatePairProbability =
|
|
StringGenerator.DEFAULT_SURROGATE_PAIR_PROBABILITY;
|
|
this.maxLength = StringGenerator.DEFAULT_MAX_LENGTH;
|
|
} else {
|
|
this.rnd = seedOrRnd;
|
|
this.surrogatePairProbability = StringGenerator.validateProbability(
|
|
surrogatePairProbability!
|
|
);
|
|
this.maxLength = StringGenerator.validateLength(maxLength!);
|
|
}
|
|
}
|
|
|
|
private static validateProbability(probability: number): number {
|
|
if (!Number.isFinite(probability)) {
|
|
throw new Error(
|
|
`invalid surrogate pair probability: ${probability} (must be between 0.0 and 1.0, inclusive)`
|
|
);
|
|
} else if (probability < 0.0) {
|
|
throw new Error(
|
|
`invalid surrogate pair probability: ${probability} (must be greater than or equal to zero)`
|
|
);
|
|
} else if (probability > 1.0) {
|
|
throw new Error(
|
|
`invalid surrogate pair probability: ${probability} (must be less than or equal to 1)`
|
|
);
|
|
}
|
|
return probability;
|
|
}
|
|
|
|
private static validateLength(length: number): number {
|
|
if (length < 0) {
|
|
throw new Error(
|
|
`invalid maximum string length: ${length} (must be greater than or equal to zero)`
|
|
);
|
|
}
|
|
return length;
|
|
}
|
|
|
|
next(): string {
|
|
const length = this.rnd.nextInt(this.maxLength + 1);
|
|
const sb = new StringBuilder();
|
|
while (sb.length() < length) {
|
|
const codePoint = this.nextCodePoint();
|
|
sb.appendCodePoint(codePoint);
|
|
}
|
|
return sb.toString();
|
|
}
|
|
|
|
private isNextSurrogatePair(): boolean {
|
|
return StringGenerator.nextBoolean(
|
|
this.rnd,
|
|
this.surrogatePairProbability
|
|
);
|
|
}
|
|
|
|
private static nextBoolean(rnd: Random, probability: number): boolean {
|
|
if (probability === 0.0) {
|
|
return false;
|
|
} else if (probability === 1.0) {
|
|
return true;
|
|
} else {
|
|
return rnd.nextFloat() < probability;
|
|
}
|
|
}
|
|
|
|
private nextCodePoint(): number {
|
|
if (this.isNextSurrogatePair()) {
|
|
return this.nextSurrogateCodePoint();
|
|
} else {
|
|
return this.nextNonSurrogateCodePoint();
|
|
}
|
|
}
|
|
|
|
private nextSurrogateCodePoint(): number {
|
|
const highSurrogateMin = 0xd800;
|
|
const highSurrogateMax = 0xdbff;
|
|
const lowSurrogateMin = 0xdc00;
|
|
const lowSurrogateMax = 0xdfff;
|
|
|
|
const highSurrogate = this.nextCodePointRange(
|
|
highSurrogateMin,
|
|
highSurrogateMax
|
|
);
|
|
const lowSurrogate = this.nextCodePointRange(
|
|
lowSurrogateMin,
|
|
lowSurrogateMax
|
|
);
|
|
|
|
return (
|
|
(highSurrogate - 0xd800) * 0x400 + (lowSurrogate - 0xdc00) + 0x10000
|
|
);
|
|
}
|
|
|
|
private nextNonSurrogateCodePoint(): number {
|
|
let codePoint;
|
|
do {
|
|
codePoint = this.nextCodePointRange(0, 0xffff); // BMP range
|
|
} while (codePoint >= 0xd800 && codePoint <= 0xdfff); // Exclude surrogate range
|
|
|
|
return codePoint;
|
|
}
|
|
|
|
private nextCodePointRange(min: number, max: number): number {
|
|
const rangeSize = max - min + 1;
|
|
const offset = this.rnd.nextInt(rangeSize);
|
|
return min + offset;
|
|
}
|
|
}
|
|
|
|
class Random {
|
|
private seed: number;
|
|
|
|
constructor(seed: number) {
|
|
this.seed = seed;
|
|
}
|
|
|
|
nextInt(max: number): number {
|
|
// Update the seed with pseudo-randomized numbers using a Linear Congruential Generator (LCG).
|
|
this.seed = (this.seed * 9301 + 49297) % 233280;
|
|
const rnd = this.seed / 233280;
|
|
return Math.floor(rnd * max);
|
|
}
|
|
|
|
nextFloat(): number {
|
|
this.seed = (this.seed * 9301 + 49297) % 233280;
|
|
return this.seed / 233280;
|
|
}
|
|
}
|
|
|
|
class StringBuilder {
|
|
private buffer: string[] = [];
|
|
|
|
append(str: string): StringBuilder {
|
|
this.buffer.push(str);
|
|
return this;
|
|
}
|
|
|
|
appendCodePoint(codePoint: number): StringBuilder {
|
|
this.buffer.push(String.fromCodePoint(codePoint));
|
|
return this;
|
|
}
|
|
|
|
toString(): string {
|
|
return this.buffer.join('');
|
|
}
|
|
|
|
length(): number {
|
|
return this.buffer.join('').length;
|
|
}
|
|
}
|
|
});
|