Files
wu-huiandGitHub 4703c99e12 merge off pipeline into main (#9929)
* chore(ci): make all firestore integration tests optional

* fix: align expressions and pipelines, address review comments, update yarn.lock, and fix package versions

* chore: update yarn.lock after merge

* fix: restore firestore package version and devDependencies to 4.15.0
2026-06-05 10:28:06 -04:00

332 lines
10 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 stringify from 'json-stable-stringify';
import { ExclusiveTestFunction, PendingTestFunction } from 'mocha';
import { Pipeline } from '../../../lite/pipelines/pipelines';
import { pipelineEq } from '../../../src/core/pipeline-util';
import { queryEquals, QueryImpl } from '../../../src/core/query';
import { targetEquals, TargetImpl } from '../../../src/core/target';
import { IndexedDbPersistence } from '../../../src/local/indexeddb_persistence';
import { debugAssert } from '../../../src/util/assert';
import { primitiveComparator } from '../../../src/util/misc';
import { addEqualityMatcher } from '../../util/equality_matcher';
import { SpecBuilder } from './spec_builder';
import { SpecStep } from './spec_test_runner';
// Disables all other tests; useful for debugging. Multiple tests can have
// this tag and they'll all be run (but all others won't).
const EXCLUSIVE_TAG = 'exclusive';
// Explicit per-platform disable flags.
const NO_WEB_TAG = 'no-web';
const NO_ANDROID_TAG = 'no-android';
const NO_IOS_TAG = 'no-ios';
// The remaining tags specify features that must be present to run a given test
// Multi-client related tests (which imply persistence).
export const MULTI_CLIENT_TAG = 'multi-client';
const EAGER_GC_TAG = 'eager-gc';
const DURABLE_PERSISTENCE_TAG = 'durable-persistence';
const BENCHMARK_TAG = 'benchmark';
const SKIP_PIPELINE_CONVERSION = 'no-pipeline-conversion';
const KNOWN_TAGS = [
BENCHMARK_TAG,
EXCLUSIVE_TAG,
MULTI_CLIENT_TAG,
NO_WEB_TAG,
NO_ANDROID_TAG,
NO_IOS_TAG,
EAGER_GC_TAG,
DURABLE_PERSISTENCE_TAG,
SKIP_PIPELINE_CONVERSION
];
// TODO(mrschmidt): Make this configurable with mocha options.
const RUN_BENCHMARK_TESTS = false;
const BENCHMARK_TEST_TIMEOUT_MS = 10 * 1000;
// The format of one describeSpec written to a JSON file.
interface SpecOutputFormat {
describeName: string;
itName: string;
tags: string[];
comment?: string;
steps: SpecStep[];
}
// The name of the describeSpec that's currently running.
let describeName = '';
// Tags for the describeSpec that's current running.
let describeTags: string[] = [];
// A map of string name -> spec json for every `it` in this `describe`.
let specsInThisTest: { [name: string]: SpecOutputFormat };
// A function to write the specs with, if set.
let writeJSONFile: ((json: string) => void) | null = null;
/**
* If you call this function before your describeSpec, then the spec test will
* be written using the given function instead of running as a normal test.
*/
export function setSpecJSONHandler(writer: (json: string) => void): void {
writeJSONFile = writer;
}
/** Gets the test runner based on the specified tags. */
function getTestRunner(
tags: string[],
persistenceEnabled: boolean,
convertToPipeline: boolean
): ExclusiveTestFunction | PendingTestFunction {
if (tags.indexOf(NO_WEB_TAG) >= 0) {
// eslint-disable-next-line no-restricted-properties
return it.skip;
} else if (
!persistenceEnabled &&
tags.indexOf(DURABLE_PERSISTENCE_TAG) !== -1
) {
// Test requires actual persistence, but it's not enabled. Skip it.
// eslint-disable-next-line no-restricted-properties
return it.skip;
} else if (persistenceEnabled && tags.indexOf(EAGER_GC_TAG) !== -1) {
// spec should have a comment explaining why it is being skipped.
// eslint-disable-next-line no-restricted-properties
return it.skip;
} else if (!persistenceEnabled && tags.indexOf(MULTI_CLIENT_TAG) !== -1) {
// eslint-disable-next-line no-restricted-properties
return it.skip;
} else if (tags.indexOf(BENCHMARK_TAG) >= 0 && !RUN_BENCHMARK_TESTS) {
// eslint-disable-next-line no-restricted-properties
return it.skip;
} else if (convertToPipeline && tags.indexOf(SKIP_PIPELINE_CONVERSION) >= 0) {
// eslint-disable-next-line no-restricted-properties
return it.skip;
} else if (tags.indexOf(EXCLUSIVE_TAG) >= 0) {
// eslint-disable-next-line no-restricted-properties
return it.only;
} else {
return it;
}
}
/** If required, returns a custom test timeout for long-running tests */
function getTestTimeout(tags: string[]): number | undefined {
if (tags.indexOf(BENCHMARK_TAG) >= 0) {
return BENCHMARK_TEST_TIMEOUT_MS;
} else {
return undefined;
}
}
/**
* Like it(), but for spec tests.
* @param name - A name to give the test.
* @param tags - Tags to apply to the test (e.g. 'exclusive' to only run
* individual tests)
* @param builder - A function that returns a spec.
* If writeToJSONFile has been called, the spec will be stored in
* `specsInThisTest`. Otherwise, it will be run, just as it() would run it.
*/
export function specTest(
name: string,
tags: string[],
builder: () => SpecBuilder
): void;
export function specTest(
name: string,
tags: string[],
comment: string,
builder: () => SpecBuilder
): void;
export function specTest(
name: string,
tags: string[],
commentOrBuilder: string | (() => SpecBuilder),
maybeBuilder?: () => SpecBuilder
): void {
let comment: string | undefined;
let builder: () => SpecBuilder;
if (typeof commentOrBuilder === 'string') {
comment = commentOrBuilder;
builder = maybeBuilder!;
} else {
builder = commentOrBuilder;
}
debugAssert(!!builder, 'Missing spec builder');
// Union in the tags for the describeSpec().
tags = tags.concat(describeTags);
for (const tag of tags) {
debugAssert(
KNOWN_TAGS.indexOf(tag) >= 0,
'Unknown tag "' + tag + '" on test: ' + name
);
}
if (!writeJSONFile) {
const persistenceModes = IndexedDbPersistence.isAvailable()
? [true, false]
: [false];
for (const usePersistence of persistenceModes) {
const convertToPipelines = [false, true];
for (const convertToPipeline of convertToPipelines) {
const runner = getTestRunner(tags, usePersistence, convertToPipeline);
const timeout = getTestTimeout(tags);
const mode = usePersistence ? '(Persistence)' : '(Memory)';
const queryMode = convertToPipeline ? '(Pipeline)' : '(Query)';
const fullName = `${mode} ${queryMode} ${name}`;
const queuedTest = runner(fullName, async () => {
const spec = builder();
const start = Date.now();
await spec.runAsTest(
fullName,
tags,
usePersistence,
convertToPipeline
);
const end = Date.now();
if (tags.indexOf(BENCHMARK_TAG) >= 0) {
// eslint-disable-next-line no-console
console.log(`Runtime: ${end - start} ms.`);
}
});
if (timeout !== undefined) {
queuedTest.timeout(timeout);
}
}
}
} else {
debugAssert(
tags.indexOf(EXCLUSIVE_TAG) === -1,
`The 'exclusive' tag is only supported for development and should not be exported to ` +
`other platforms.`
);
const spec = builder();
const specJSON = spec.toJSON();
const json = {
describeName,
itName: name,
tags,
comment,
config: specJSON.config,
steps: specJSON.steps
};
if (name in specsInThisTest) {
throw new Error('duplicate spec test: "' + name + '"');
}
specsInThisTest[name] = json;
}
}
/**
* Like describe, but for spec tests.
* @param name - A name to give the test.
* @param tags - Tags to apply to all tests in the spec (e.g. 'exclusive' to
* only run individual tests)
* @param builder - A function that calls specTest for each test case.
* If writeToJSONFile has been called, the specs will be stored in
* that file. Otherwise, they will be run, just as describe would run.
*/
export function describeSpec(
name: string,
tags: string[],
builder: () => void
): void {
describeTags = tags;
describeName = name;
if (!writeJSONFile) {
describe(name, () => {
addEqualityMatcher(
{ equalsFn: targetEquals, forType: TargetImpl },
{ equalsFn: queryEquals, forType: QueryImpl },
{ equalsFn: pipelineEq, forType: Pipeline }
);
return builder();
});
} else {
specsInThisTest = {};
builder();
// Note: We use json-stable-stringify instead of JSON.stringify() to ensure
// that the generated JSON does not produce diffs merely due to the order
// of the keys in an object changing.
const output = stringify(specsInThisTest, {
space: 2,
cmp: stringifyComparator
});
writeJSONFile(output || '');
}
}
/**
* The key ordering overrides used when sorting keys in the generated JSON.
* If both keys being compared are present in this array then they should be
* ordered in the generated JSON in the same relative order of this array.
*/
const stringifyCustomOrdering = [
'comment',
'describeName',
'itName',
'tags',
'config',
'steps'
];
/**
* Assigns a "group number" to the given key in the generated JSON.
*
* Keys with lower group numbers should be ordered before keys with greater
* group numbers. Keys with equal group numbers should be ordered
* alphabetically.
*/
function stringifyGroup(s: string): number {
const index = stringifyCustomOrdering.indexOf(s);
if (index >= 0) {
return index;
} else if (!s.startsWith('expected')) {
return stringifyCustomOrdering.length;
} else {
return stringifyCustomOrdering.length + 1;
}
}
/**
* A comparator function for stringify() that sorts keys in the JSON output.
*
* In order to produce JSON that has somewhat-intuitively-ordered keys, this
* comparator intentionally deviates from pure alphabetic ordering, placing
* some logically-first keys before others.
*/
function stringifyComparator(
a: { key: string; value: unknown },
b: { key: string; value: unknown }
): number {
const aGroup = stringifyGroup(a.key);
const bGroup = stringifyGroup(b.key);
if (aGroup === bGroup) {
return primitiveComparator(a.key, b.key);
} else {
return primitiveComparator(aGroup, bGroup);
}
}