diff --git a/docs/development/core/server/kibana-plugin-core-server.mergesavedobjectmigrationmaps.md b/docs/development/core/server/kibana-plugin-core-server.mergesavedobjectmigrationmaps.md
new file mode 100644
index 0000000000000..68cd580b57882
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-core-server.mergesavedobjectmigrationmaps.md
@@ -0,0 +1,21 @@
+
+
+[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [mergeSavedObjectMigrationMaps](./kibana-plugin-core-server.mergesavedobjectmigrationmaps.md)
+
+## mergeSavedObjectMigrationMaps variable
+
+Merges two saved object migration maps.
+
+If there is a migration for a given version on only one of the maps, that migration function will be used:
+
+mergeSavedObjectMigrationMaps({ '1.2.3': f }, { '4.5.6': g }) -> { '1.2.3': f, '4.5.6': g }
+
+If there is a migration for a given version on both maps, the migrations will be composed:
+
+mergeSavedObjectMigrationMaps({ '1.2.3': f }, { '1.2.3': g }) -> { '1.2.3': (doc, context) => f(g(doc, context), context) }
+
+Signature:
+
+```typescript
+mergeSavedObjectMigrationMaps: (map1: SavedObjectMigrationMap, map2: SavedObjectMigrationMap) => SavedObjectMigrationMap
+```
diff --git a/package.json b/package.json
index c60412dd7896d..793f8d3257ea1 100644
--- a/package.json
+++ b/package.json
@@ -447,6 +447,7 @@
"@emotion/babel-preset-css-prop": "^11.2.0",
"@emotion/jest": "^11.3.0",
"@istanbuljs/schema": "^0.1.2",
+ "@jest/console": "^26.6.2",
"@jest/reporters": "^26.6.2",
"@kbn/babel-code-parser": "link:bazel-bin/packages/kbn-babel-code-parser",
"@kbn/babel-preset": "link:bazel-bin/packages/kbn-babel-preset",
diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts
index 3dff5acdc228a..f16cdcc80f286 100644
--- a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts
+++ b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts
@@ -11,16 +11,28 @@ import Os from 'os';
import Fs from 'fs';
import Path from 'path';
import crypto from 'crypto';
+
import execa from 'execa';
-import Axios from 'axios';
+import Axios, { AxiosRequestConfig } from 'axios';
// @ts-expect-error not "public", but necessary to prevent Jest shimming from breaking things
import httpAdapter from 'axios/lib/adapters/http';
import { ToolingLog } from '../tooling_log';
import { parseConfig, Config } from './ci_stats_config';
+import type { CiStatsTestGroupInfo, CiStatsTestRun } from './ci_stats_test_group_types';
const BASE_URL = 'https://ci-stats.kibana.dev';
+/** Container for metadata that can be attached to different ci-stats objects */
+export interface CiStatsMetadata {
+ /**
+ * Arbitrary key-value pairs which can be attached to CiStatsTiming and CiStatsMetric
+ * objects stored in the ci-stats service
+ */
+ [key: string]: string | string[] | number | boolean | undefined;
+}
+
+/** A ci-stats metric record */
export interface CiStatsMetric {
/** Top-level categorization for the metric, e.g. "page load bundle size" */
group: string;
@@ -40,13 +52,7 @@ export interface CiStatsMetric {
meta?: CiStatsMetadata;
}
-export interface CiStatsMetadata {
- /**
- * Arbitrary key-value pairs which can be attached to CiStatsTiming and CiStatsMetric
- * objects stored in the ci-stats service
- */
- [key: string]: string | string[] | number | boolean | undefined;
-}
+/** A ci-stats timing event */
export interface CiStatsTiming {
/** Top-level categorization for the timing, e.g. "scripts/foo", process type, etc. */
group: string;
@@ -58,13 +64,7 @@ export interface CiStatsTiming {
meta?: CiStatsMetadata;
}
-interface ReqOptions {
- auth: boolean;
- path: string;
- body: any;
- bodyDesc: string;
-}
-
+/** Options for reporting timings to ci-stats */
export interface TimingsOptions {
/** list of timings to record */
timings: CiStatsTiming[];
@@ -74,10 +74,41 @@ export interface TimingsOptions {
kibanaUuid?: string | null;
}
+/** Options for reporting metrics to ci-stats */
export interface MetricsOptions {
/** Default metadata to add to each metric */
defaultMeta?: CiStatsMetadata;
}
+
+/** Options for reporting tests to ci-stats */
+export interface CiStatsReportTestsOptions {
+ /**
+ * Information about the group of tests that were run
+ */
+ group: CiStatsTestGroupInfo;
+ /**
+ * Information about each test that ran, including failure information
+ */
+ testRuns: CiStatsTestRun[];
+}
+
+/* @internal */
+interface ReportTestsResponse {
+ buildId: string;
+ groupId: string;
+ testRunCount: number;
+}
+
+/* @internal */
+interface ReqOptions {
+ auth: boolean;
+ path: string;
+ body: any;
+ bodyDesc: string;
+ query?: AxiosRequestConfig['params'];
+}
+
+/** Object that helps report data to the ci-stats service */
export class CiStatsReporter {
/**
* Create a CiStatsReporter by inspecting the ENV for the necessary config
@@ -86,7 +117,7 @@ export class CiStatsReporter {
return new CiStatsReporter(parseConfig(log), log);
}
- constructor(private config: Config | undefined, private log: ToolingLog) {}
+ constructor(private readonly config: Config | undefined, private readonly log: ToolingLog) {}
/**
* Determine if CI_STATS is explicitly disabled by the environment. To determine
@@ -165,7 +196,7 @@ export class CiStatsReporter {
this.log.debug('CIStatsReporter committerHash: %s', defaultMeta.committerHash);
- return await this.req({
+ return !!(await this.req({
auth: !!buildId,
path: '/v1/timings',
body: {
@@ -175,7 +206,7 @@ export class CiStatsReporter {
timings,
},
bodyDesc: timings.length === 1 ? `${timings.length} timing` : `${timings.length} timings`,
- });
+ }));
}
/**
@@ -188,12 +219,11 @@ export class CiStatsReporter {
}
const buildId = this.config?.buildId;
-
if (!buildId) {
- throw new Error(`CiStatsReporter can't be authorized without a buildId`);
+ throw new Error(`metrics can't be reported without a buildId`);
}
- return await this.req({
+ return !!(await this.req({
auth: true,
path: '/v1/metrics',
body: {
@@ -204,6 +234,30 @@ export class CiStatsReporter {
bodyDesc: `metrics: ${metrics
.map(({ group, id, value }) => `[${group}/${id}=${value}]`)
.join(' ')}`,
+ }));
+ }
+
+ /**
+ * Send test reports to ci-stats
+ */
+ async reportTests({ group, testRuns }: CiStatsReportTestsOptions) {
+ if (!this.config?.buildId || !this.config?.apiToken) {
+ throw new Error(
+ 'unable to report tests unless buildId is configured and auth config available'
+ );
+ }
+
+ return await this.req({
+ auth: true,
+ path: '/v1/test_group',
+ query: {
+ buildId: this.config?.buildId,
+ },
+ bodyDesc: `[${group.name}/${group.type}] test groups with ${testRuns.length} tests`,
+ body: [
+ JSON.stringify({ group }),
+ ...testRuns.map((testRun) => JSON.stringify({ testRun })),
+ ].join('\n'),
});
}
@@ -241,7 +295,7 @@ export class CiStatsReporter {
}
}
- private async req({ auth, body, bodyDesc, path }: ReqOptions) {
+ private async req({ auth, body, bodyDesc, path, query }: ReqOptions) {
let attempt = 0;
const maxAttempts = 5;
@@ -251,23 +305,24 @@ export class CiStatsReporter {
Authorization: `token ${this.config.apiToken}`,
};
} else if (auth) {
- throw new Error('this.req() shouldnt be called with auth=true if this.config is defined');
+ throw new Error('this.req() shouldnt be called with auth=true if this.config is not defined');
}
while (true) {
attempt += 1;
try {
- await Axios.request({
+ const resp = await Axios.request({
method: 'POST',
url: path,
baseURL: BASE_URL,
headers,
data: body,
+ params: query,
adapter: httpAdapter,
});
- return true;
+ return resp.data;
} catch (error) {
if (!error?.request) {
// not an axios error, must be a usage error that we should notify user about
diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_test_group_types.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_test_group_types.ts
new file mode 100644
index 0000000000000..147d4e19325b2
--- /dev/null
+++ b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_test_group_types.ts
@@ -0,0 +1,90 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import type { CiStatsMetadata } from './ci_stats_reporter';
+
+export type CiStatsTestResult = 'fail' | 'pass' | 'skip';
+export type CiStatsTestType =
+ | 'after all hook'
+ | 'after each hook'
+ | 'before all hook'
+ | 'before each hook'
+ | 'test';
+
+export interface CiStatsTestRun {
+ /**
+ * ISO-8601 formatted datetime representing when the tests started running
+ */
+ startTime: string;
+ /**
+ * Duration of the tests in milliseconds
+ */
+ durationMs: number;
+ /**
+ * A sequence number, this is used to order the tests in a specific test run
+ */
+ seq: number;
+ /**
+ * The type of this "test run", usually this is just "test" but when reporting issues in hooks it can be set to the type of hook
+ */
+ type: CiStatsTestType;
+ /**
+ * "fail", "pass" or "skip", the result of the tests
+ */
+ result: CiStatsTestResult;
+ /**
+ * The list of suite names containing this test, the first being the outermost suite
+ */
+ suites: string[];
+ /**
+ * The name of this specific test run
+ */
+ name: string;
+ /**
+ * Relative path from the root of the repo contianing this test
+ */
+ file: string;
+ /**
+ * Error message if the test failed
+ */
+ error?: string;
+ /**
+ * Debug output/stdout produced by the test
+ */
+ stdout?: string;
+ /**
+ * Screenshots captured during the test run
+ */
+ screenshots?: Array<{
+ name: string;
+ base64Png: string;
+ }>;
+}
+
+export interface CiStatsTestGroupInfo {
+ /**
+ * ISO-8601 formatted datetime representing when the group of tests started running
+ */
+ startTime: string;
+ /**
+ * The number of miliseconds that the tests ran for
+ */
+ durationMs: number;
+ /**
+ * The type of tests run in this group, any value is valid but test groups are groupped by type in the UI so use something consistent
+ */
+ type: string;
+ /**
+ * The name of this specific group (within the "type")
+ */
+ name: string;
+ /**
+ * Arbitrary metadata associated with this group. We currently look for a ciGroup metadata property for highlighting that when appropriate
+ */
+ meta: CiStatsMetadata;
+}
diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts
index 318a2921517f1..cf80d06613dbf 100644
--- a/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts
+++ b/packages/kbn-dev-utils/src/ci_stats_reporter/index.ts
@@ -10,3 +10,4 @@ export * from './ci_stats_reporter';
export type { Config } from './ci_stats_config';
export * from './ship_ci_stats_cli';
export { getTimeReporter } from './report_time';
+export * from './ci_stats_test_group_types';
diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js
index 146c3fe9f8d58..d41bbe2480ebc 100644
--- a/packages/kbn-pm/dist/index.js
+++ b/packages/kbn-pm/dist/index.js
@@ -9048,7 +9048,9 @@ var _ci_stats_config = __webpack_require__(218);
*/
// @ts-expect-error not "public", but necessary to prevent Jest shimming from breaking things
const BASE_URL = 'https://ci-stats.kibana.dev';
+/** Container for metadata that can be attached to different ci-stats objects */
+/** Object that helps report data to the ci-stats service */
class CiStatsReporter {
/**
* Create a CiStatsReporter by inspecting the ENV for the necessary config
@@ -9145,7 +9147,7 @@ class CiStatsReporter {
totalMem: _os.default.totalmem()
};
this.log.debug('CIStatsReporter committerHash: %s', defaultMeta.committerHash);
- return await this.req({
+ return !!(await this.req({
auth: !!buildId,
path: '/v1/timings',
body: {
@@ -9155,7 +9157,7 @@ class CiStatsReporter {
timings
},
bodyDesc: timings.length === 1 ? `${timings.length} timing` : `${timings.length} timings`
- });
+ }));
}
/**
* Report metrics data to the ci-stats service. If running outside of CI this method
@@ -9173,10 +9175,10 @@ class CiStatsReporter {
const buildId = (_this$config4 = this.config) === null || _this$config4 === void 0 ? void 0 : _this$config4.buildId;
if (!buildId) {
- throw new Error(`CiStatsReporter can't be authorized without a buildId`);
+ throw new Error(`metrics can't be reported without a buildId`);
}
- return await this.req({
+ return !!(await this.req({
auth: true,
path: '/v1/metrics',
body: {
@@ -9189,6 +9191,35 @@ class CiStatsReporter {
id,
value
}) => `[${group}/${id}=${value}]`).join(' ')}`
+ }));
+ }
+ /**
+ * Send test reports to ci-stats
+ */
+
+
+ async reportTests({
+ group,
+ testRuns
+ }) {
+ var _this$config5, _this$config6, _this$config7;
+
+ if (!((_this$config5 = this.config) !== null && _this$config5 !== void 0 && _this$config5.buildId) || !((_this$config6 = this.config) !== null && _this$config6 !== void 0 && _this$config6.apiToken)) {
+ throw new Error('unable to report tests unless buildId is configured and auth config available');
+ }
+
+ return await this.req({
+ auth: true,
+ path: '/v1/test_group',
+ query: {
+ buildId: (_this$config7 = this.config) === null || _this$config7 === void 0 ? void 0 : _this$config7.buildId
+ },
+ bodyDesc: `[${group.name}/${group.type}] test groups with ${testRuns.length} tests`,
+ body: [JSON.stringify({
+ group
+ }), ...testRuns.map(testRun => JSON.stringify({
+ testRun
+ }))].join('\n')
});
}
/**
@@ -9238,7 +9269,8 @@ class CiStatsReporter {
auth,
body,
bodyDesc,
- path
+ path,
+ query
}) {
let attempt = 0;
const maxAttempts = 5;
@@ -9249,22 +9281,23 @@ class CiStatsReporter {
Authorization: `token ${this.config.apiToken}`
};
} else if (auth) {
- throw new Error('this.req() shouldnt be called with auth=true if this.config is defined');
+ throw new Error('this.req() shouldnt be called with auth=true if this.config is not defined');
}
while (true) {
attempt += 1;
try {
- await _axios.default.request({
+ const resp = await _axios.default.request({
method: 'POST',
url: path,
baseURL: BASE_URL,
headers,
data: body,
+ params: query,
adapter: _http.default
});
- return true;
+ return resp.data;
} catch (error) {
var _error$response;
diff --git a/packages/kbn-test/BUILD.bazel b/packages/kbn-test/BUILD.bazel
index e8b3348c22663..bc2314dabe54d 100644
--- a/packages/kbn-test/BUILD.bazel
+++ b/packages/kbn-test/BUILD.bazel
@@ -41,8 +41,10 @@ RUNTIME_DEPS = [
"//packages/kbn-std",
"//packages/kbn-utils",
"@npm//@elastic/elasticsearch",
- "@npm//axios",
"@npm//@babel/traverse",
+ "@npm//@jest/console",
+ "@npm//@jest/reporters",
+ "@npm//axios",
"@npm//chance",
"@npm//del",
"@npm//enzyme",
@@ -56,7 +58,6 @@ RUNTIME_DEPS = [
"@npm//jest-cli",
"@npm//jest-snapshot",
"@npm//jest-styled-components",
- "@npm//@jest/reporters",
"@npm//joi",
"@npm//mustache",
"@npm//parse-link-header",
@@ -77,6 +78,10 @@ TYPES_DEPS = [
"//packages/kbn-i18n",
"//packages/kbn-utils",
"@npm//@elastic/elasticsearch",
+ "@npm//@jest/console",
+ "@npm//@jest/reporters",
+ "@npm//axios",
+ "@npm//elastic-apm-node",
"@npm//del",
"@npm//form-data",
"@npm//jest",
diff --git a/packages/kbn-test/jest-preset.js b/packages/kbn-test/jest-preset.js
index db64f070b37d9..a4886676e1b86 100644
--- a/packages/kbn-test/jest-preset.js
+++ b/packages/kbn-test/jest-preset.js
@@ -54,6 +54,12 @@ module.exports = {
rootDirectory: '.',
},
],
+ [
+ '@kbn/test/target_node/jest/ci_stats_jest_reporter',
+ {
+ testGroupType: 'Jest Unit Tests',
+ },
+ ],
],
// The paths to modules that run some code to configure or set up the testing environment before each test
diff --git a/packages/kbn-test/jest_integration/jest-preset.js b/packages/kbn-test/jest_integration/jest-preset.js
index 7504dec9e7a20..be007262477d3 100644
--- a/packages/kbn-test/jest_integration/jest-preset.js
+++ b/packages/kbn-test/jest_integration/jest-preset.js
@@ -21,6 +21,12 @@ module.exports = {
reporters: [
'default',
['@kbn/test/target_node/jest/junit_reporter', { reportName: 'Jest Integration Tests' }],
+ [
+ '@kbn/test/target_node/jest/ci_stats_jest_reporter',
+ {
+ testGroupType: 'Jest Integration Tests',
+ },
+ ],
],
coverageReporters: !!process.env.CI
? [['json', { file: 'jest-integration.json' }]]
diff --git a/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts b/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts
index 27445e68f9537..527c89c969a08 100644
--- a/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts
+++ b/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts
@@ -13,7 +13,7 @@ import { Suite, Test } from './fake_mocha_types';
import {
Lifecycle,
LifecyclePhase,
- FailureMetadata,
+ TestMetadata,
readConfigFile,
ProviderCollection,
readProviderSpec,
@@ -27,7 +27,7 @@ import {
export class FunctionalTestRunner {
public readonly lifecycle = new Lifecycle();
- public readonly failureMetadata = new FailureMetadata(this.lifecycle);
+ public readonly testMetadata = new TestMetadata(this.lifecycle);
private closed = false;
private readonly esVersion: EsVersion;
@@ -181,7 +181,7 @@ export class FunctionalTestRunner {
const coreProviders = readProviderSpec('Service', {
lifecycle: () => this.lifecycle,
log: () => this.log,
- failureMetadata: () => this.failureMetadata,
+ testMetadata: () => this.testMetadata,
config: () => config,
dockerServers: () => dockerServers,
esVersion: () => this.esVersion,
diff --git a/packages/kbn-test/src/functional_test_runner/index.ts b/packages/kbn-test/src/functional_test_runner/index.ts
index 1718b5f7a4bc5..e67e72fd5801a 100644
--- a/packages/kbn-test/src/functional_test_runner/index.ts
+++ b/packages/kbn-test/src/functional_test_runner/index.ts
@@ -7,7 +7,8 @@
*/
export { FunctionalTestRunner } from './functional_test_runner';
-export { readConfigFile, Config, EsVersion } from './lib';
+export { readConfigFile, Config, EsVersion, Lifecycle, LifecyclePhase } from './lib';
+export type { ScreenshotRecord } from './lib';
export { runFtrCli } from './cli';
export * from './lib/docker_servers';
export * from './public_types';
diff --git a/packages/kbn-test/src/functional_test_runner/integration_tests/__fixtures__/failure_hooks/config.js b/packages/kbn-test/src/functional_test_runner/integration_tests/__fixtures__/failure_hooks/config.js
index 0b9cfd88b4cbb..b6aa2669be681 100644
--- a/packages/kbn-test/src/functional_test_runner/integration_tests/__fixtures__/failure_hooks/config.js
+++ b/packages/kbn-test/src/functional_test_runner/integration_tests/__fixtures__/failure_hooks/config.js
@@ -35,6 +35,7 @@ export default function () {
},
mochaReporter: {
captureLogOutput: false,
+ sendToCiStats: false,
},
};
}
diff --git a/packages/kbn-test/src/functional_test_runner/integration_tests/__fixtures__/simple_project/config.js b/packages/kbn-test/src/functional_test_runner/integration_tests/__fixtures__/simple_project/config.js
index 7163058b78523..4c87b53b5753b 100644
--- a/packages/kbn-test/src/functional_test_runner/integration_tests/__fixtures__/simple_project/config.js
+++ b/packages/kbn-test/src/functional_test_runner/integration_tests/__fixtures__/simple_project/config.js
@@ -10,4 +10,7 @@ import { resolve } from 'path';
export default () => ({
testFiles: [resolve(__dirname, 'tests.js')],
+ mochaReporter: {
+ sendToCiStats: false,
+ },
});
diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/config.ts b/packages/kbn-test/src/functional_test_runner/lib/config/config.ts
index 1d4af9c33fb79..d6248b9628e73 100644
--- a/packages/kbn-test/src/functional_test_runner/lib/config/config.ts
+++ b/packages/kbn-test/src/functional_test_runner/lib/config/config.ts
@@ -20,6 +20,7 @@ interface Options {
}
export class Config {
+ public readonly path: string;
private [$values]: Record;
constructor(options: Options) {
@@ -29,6 +30,7 @@ export class Config {
throw new TypeError('path is a required option');
}
+ this.path = path;
const { error, value } = schema.validate(settings, {
abortEarly: false,
context: {
diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts
index a9ceaa643a60f..e51ebc4538343 100644
--- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts
+++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts
@@ -152,6 +152,7 @@ export const schema = Joi.object()
mochaReporter: Joi.object()
.keys({
captureLogOutput: Joi.boolean().default(!!process.env.CI),
+ sendToCiStats: Joi.boolean().default(!!process.env.CI),
})
.default(),
diff --git a/packages/kbn-test/src/functional_test_runner/lib/failure_metadata.test.ts b/packages/kbn-test/src/functional_test_runner/lib/failure_metadata.test.ts
deleted file mode 100644
index b40f6a5c83688..0000000000000
--- a/packages/kbn-test/src/functional_test_runner/lib/failure_metadata.test.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-import { Lifecycle } from './lifecycle';
-import { FailureMetadata } from './failure_metadata';
-import { Test } from '../fake_mocha_types';
-
-it('collects metadata for the current test', async () => {
- const lifecycle = new Lifecycle();
- const failureMetadata = new FailureMetadata(lifecycle);
-
- const test1 = {} as Test;
- await lifecycle.beforeEachRunnable.trigger(test1);
- failureMetadata.add({ foo: 'bar' });
-
- expect(failureMetadata.get(test1)).toMatchInlineSnapshot(`
- Object {
- "foo": "bar",
- }
- `);
-
- const test2 = {} as Test;
- await lifecycle.beforeEachRunnable.trigger(test2);
- failureMetadata.add({ test: 2 });
-
- expect(failureMetadata.get(test1)).toMatchInlineSnapshot(`
- Object {
- "foo": "bar",
- }
- `);
- expect(failureMetadata.get(test2)).toMatchInlineSnapshot(`
- Object {
- "test": 2,
- }
- `);
-});
-
-it('adds messages to the messages state', () => {
- const lifecycle = new Lifecycle();
- const failureMetadata = new FailureMetadata(lifecycle);
-
- const test1 = {} as Test;
- lifecycle.beforeEachRunnable.trigger(test1);
- failureMetadata.addMessages(['foo', 'bar']);
- failureMetadata.addMessages(['baz']);
-
- expect(failureMetadata.get(test1)).toMatchInlineSnapshot(`
- Object {
- "messages": Array [
- "foo",
- "bar",
- "baz",
- ],
- }
- `);
-});
diff --git a/packages/kbn-test/src/functional_test_runner/lib/failure_metadata.ts b/packages/kbn-test/src/functional_test_runner/lib/failure_metadata.ts
deleted file mode 100644
index a766c73f4c727..0000000000000
--- a/packages/kbn-test/src/functional_test_runner/lib/failure_metadata.ts
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * 2.0 and the Server Side Public License, v 1; you may not use this file except
- * in compliance with, at your election, the Elastic License 2.0 or the Server
- * Side Public License, v 1.
- */
-
-import Path from 'path';
-
-import { REPO_ROOT } from '@kbn/utils';
-
-import { Lifecycle } from './lifecycle';
-
-interface Metadata {
- [key: string]: unknown;
-}
-
-export class FailureMetadata {
- // mocha's global types mean we can't import Mocha or it will override the global jest types..............
- private currentRunnable?: any;
- private readonly allMetadata = new Map();
-
- constructor(lifecycle: Lifecycle) {
- if (!process.env.GCS_UPLOAD_PREFIX && process.env.CI) {
- throw new Error(
- 'GCS_UPLOAD_PREFIX environment variable is not set and must always be set on CI'
- );
- }
-
- lifecycle.beforeEachRunnable.add((runnable) => {
- this.currentRunnable = runnable;
- });
- }
-
- add(metadata: Metadata | ((current: Metadata) => Metadata)) {
- if (!this.currentRunnable) {
- throw new Error('no current runnable to associate metadata with');
- }
-
- const current = this.allMetadata.get(this.currentRunnable);
- this.allMetadata.set(this.currentRunnable, {
- ...current,
- ...(typeof metadata === 'function' ? metadata(current || {}) : metadata),
- });
- }
-
- addMessages(messages: string[]) {
- this.add((current) => ({
- messages: [...(Array.isArray(current.messages) ? current.messages : []), ...messages],
- }));
- }
-
- /**
- * @param name Name to label the URL with
- * @param repoPath absolute path, within the repo, that will be uploaded
- */
- addScreenshot(name: string, repoPath: string) {
- const prefix = process.env.GCS_UPLOAD_PREFIX;
-
- if (!prefix) {
- return;
- }
-
- const slash = prefix.endsWith('/') ? '' : '/';
- const urlPath = Path.relative(REPO_ROOT, repoPath)
- .split(Path.sep)
- .map((c) => encodeURIComponent(c))
- .join('/');
-
- if (urlPath.startsWith('..')) {
- throw new Error(
- `Only call addUploadLink() with paths that are within the repo root, received ${repoPath} and repo root is ${REPO_ROOT}`
- );
- }
-
- const url = `https://storage.googleapis.com/${prefix}${slash}${urlPath}`;
- const screenshot = {
- name,
- url,
- };
-
- this.add((current) => ({
- screenshots: [...(Array.isArray(current.screenshots) ? current.screenshots : []), screenshot],
- }));
-
- return screenshot;
- }
-
- get(runnable: any) {
- return this.allMetadata.get(runnable);
- }
-}
diff --git a/packages/kbn-test/src/functional_test_runner/lib/index.ts b/packages/kbn-test/src/functional_test_runner/lib/index.ts
index 98b5fec0597e4..e387fd156fe8a 100644
--- a/packages/kbn-test/src/functional_test_runner/lib/index.ts
+++ b/packages/kbn-test/src/functional_test_runner/lib/index.ts
@@ -12,7 +12,7 @@ export { readConfigFile, Config } from './config';
export { readProviderSpec, ProviderCollection } from './providers';
// @internal
export { runTests, setupMocha } from './mocha';
-export { FailureMetadata } from './failure_metadata';
+export * from './test_metadata';
export * from './docker_servers';
export { SuiteTracker } from './suite_tracker';
diff --git a/packages/kbn-test/src/functional_test_runner/lib/lifecycle.ts b/packages/kbn-test/src/functional_test_runner/lib/lifecycle.ts
index 17dcaa8d7447d..e683ec23a8d84 100644
--- a/packages/kbn-test/src/functional_test_runner/lib/lifecycle.ts
+++ b/packages/kbn-test/src/functional_test_runner/lib/lifecycle.ts
@@ -11,15 +11,23 @@ import { LifecyclePhase } from './lifecycle_phase';
import { Suite, Test } from '../fake_mocha_types';
export class Lifecycle {
+ /** lifecycle phase that will run handlers once before tests execute */
public readonly beforeTests = new LifecyclePhase<[Suite]>({
singular: true,
});
+ /** lifecycle phase that runs handlers before each runnable (test and hooks) */
public readonly beforeEachRunnable = new LifecyclePhase<[Test]>();
+ /** lifecycle phase that runs handlers before each suite */
public readonly beforeTestSuite = new LifecyclePhase<[Suite]>();
+ /** lifecycle phase that runs handlers before each test */
public readonly beforeEachTest = new LifecyclePhase<[Test]>();
+ /** lifecycle phase that runs handlers after each suite */
public readonly afterTestSuite = new LifecyclePhase<[Suite]>();
+ /** lifecycle phase that runs handlers after a test fails */
public readonly testFailure = new LifecyclePhase<[Error, Test]>();
+ /** lifecycle phase that runs handlers after a hook fails */
public readonly testHookFailure = new LifecyclePhase<[Error, Test]>();
+ /** lifecycle phase that runs handlers at the very end of execution */
public readonly cleanup = new LifecyclePhase<[]>({
singular: true,
});
diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/ci_stats_ftr_reporter.ts b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/ci_stats_ftr_reporter.ts
new file mode 100644
index 0000000000000..61eb7eccce430
--- /dev/null
+++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/ci_stats_ftr_reporter.ts
@@ -0,0 +1,157 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import * as Path from 'path';
+
+import { REPO_ROOT } from '@kbn/utils';
+import { CiStatsReporter, CiStatsReportTestsOptions, CiStatsTestType } from '@kbn/dev-utils';
+
+import { Config } from '../../config';
+import { Runner } from '../../../fake_mocha_types';
+import { TestMetadata, ScreenshotRecord } from '../../test_metadata';
+import { Lifecycle } from '../../lifecycle';
+import { getSnapshotOfRunnableLogs } from '../../../../mocha';
+
+interface Suite {
+ _beforeAll: Runnable[];
+ _beforeEach: Runnable[];
+ _afterEach: Runnable[];
+ _afterAll: Runnable[];
+}
+
+interface Runnable {
+ isFailed(): boolean;
+ isPending(): boolean;
+ duration?: number;
+ titlePath(): string[];
+ file: string;
+ title: string;
+ parent: Suite;
+ _screenshots?: ScreenshotRecord[];
+}
+
+function getHookType(hook: Runnable): CiStatsTestType {
+ if (hook.parent._afterAll.includes(hook)) {
+ return 'after all hook';
+ }
+ if (hook.parent._afterEach.includes(hook)) {
+ return 'after each hook';
+ }
+ if (hook.parent._beforeEach.includes(hook)) {
+ return 'before each hook';
+ }
+ if (hook.parent._beforeAll.includes(hook)) {
+ return 'before all hook';
+ }
+
+ throw new Error(`unable to determine hook type, hook is not owned by it's parent`);
+}
+
+export function setupCiStatsFtrTestGroupReporter({
+ config,
+ lifecycle,
+ runner,
+ testMetadata,
+ reporter,
+}: {
+ config: Config;
+ lifecycle: Lifecycle;
+ runner: Runner;
+ testMetadata: TestMetadata;
+ reporter: CiStatsReporter;
+}) {
+ let startMs: number | undefined;
+ runner.on('start', () => {
+ startMs = Date.now();
+ });
+
+ const start = Date.now();
+ const group: CiStatsReportTestsOptions['group'] = {
+ startTime: new Date(start).toJSON(),
+ durationMs: 0,
+ type: config.path.startsWith('x-pack') ? 'X-Pack Functional Tests' : 'Functional Tests',
+ name: Path.relative(REPO_ROOT, config.path),
+ meta: {
+ ciGroup: config.get('suiteTags.include').find((t: string) => t.startsWith('ciGroup')),
+ tags: [
+ ...config.get('suiteTags.include'),
+ ...config.get('suiteTags.exclude').map((t: string) => `-${t}`),
+ ].filter((t) => !t.startsWith('ciGroup')),
+ },
+ };
+
+ const testRuns: CiStatsReportTestsOptions['testRuns'] = [];
+ function trackRunnable(
+ runnable: Runnable,
+ { error, type }: { error?: Error; type: CiStatsTestType }
+ ) {
+ testRuns.push({
+ startTime: new Date(Date.now() - (runnable.duration ?? 0)).toJSON(),
+ durationMs: runnable.duration ?? 0,
+ seq: testRuns.length + 1,
+ file: Path.relative(REPO_ROOT, runnable.file),
+ name: runnable.title,
+ suites: runnable.titlePath().slice(0, -1),
+ result: runnable.isFailed() ? 'fail' : runnable.isPending() ? 'skip' : 'pass',
+ type,
+ error: error?.stack,
+ stdout: getSnapshotOfRunnableLogs(runnable),
+ screenshots: testMetadata.getScreenshots(runnable).map((s) => ({
+ base64Png: s.base64Png,
+ name: s.name,
+ })),
+ });
+ }
+
+ const errors = new Map();
+ runner.on('fail', (test: Runnable, error: Error) => {
+ errors.set(test, error);
+ });
+
+ runner.on('hook end', (hook: Runnable) => {
+ if (hook.isFailed()) {
+ const error = errors.get(hook);
+ if (!error) {
+ throw new Error(`no error recorded for failed hook`);
+ }
+
+ trackRunnable(hook, {
+ type: getHookType(hook),
+ error,
+ });
+ }
+ });
+
+ runner.on('test end', (test: Runnable) => {
+ const error = errors.get(test);
+ if (test.isFailed() && !error) {
+ throw new Error('no error recorded for failed test');
+ }
+
+ trackRunnable(test, {
+ type: 'test',
+ error,
+ });
+ });
+
+ runner.on('end', () => {
+ if (!startMs) {
+ throw new Error('startMs was not defined');
+ }
+
+ // update the durationMs
+ group.durationMs = Date.now() - startMs;
+ });
+
+ lifecycle.cleanup.add(async () => {
+ await reporter.reportTests({
+ group,
+ testRuns,
+ });
+ });
+}
diff --git a/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js
index d6045b71bf3a7..84299cba14eaa 100644
--- a/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js
+++ b/packages/kbn-test/src/functional_test_runner/lib/mocha/reporter/reporter.js
@@ -9,7 +9,7 @@
import { format } from 'util';
import Mocha from 'mocha';
-import { ToolingLogTextWriter } from '@kbn/dev-utils';
+import { ToolingLogTextWriter, CiStatsReporter } from '@kbn/dev-utils';
import moment from 'moment';
import { recordLog, snapshotLogsForRunnable, setupJUnitReportGeneration } from '../../../../mocha';
@@ -17,11 +17,13 @@ import * as colors from './colors';
import * as symbols from './symbols';
import { ms } from './ms';
import { writeEpilogue } from './write_epilogue';
+import { setupCiStatsFtrTestGroupReporter } from './ci_stats_ftr_reporter';
export function MochaReporterProvider({ getService }) {
const log = getService('log');
const config = getService('config');
- const failureMetadata = getService('failureMetadata');
+ const lifecycle = getService('lifecycle');
+ const testMetadata = getService('testMetadata');
let originalLogWriters;
let reporterCaptureStartTime;
@@ -45,9 +47,23 @@ export function MochaReporterProvider({ getService }) {
if (config.get('junit.enabled') && config.get('junit.reportName')) {
setupJUnitReportGeneration(runner, {
reportName: config.get('junit.reportName'),
- getTestMetadata: (t) => failureMetadata.get(t),
});
}
+
+ if (config.get('mochaReporter.sendToCiStats')) {
+ const reporter = CiStatsReporter.fromEnv(log);
+ if (!reporter.hasBuildConfig()) {
+ log.warning('ci-stats reporter config is not available so test results will not be sent');
+ } else {
+ setupCiStatsFtrTestGroupReporter({
+ reporter,
+ config,
+ lifecycle,
+ runner,
+ testMetadata,
+ });
+ }
+ }
}
onStart = () => {
diff --git a/packages/kbn-test/src/functional_test_runner/lib/test_metadata.ts b/packages/kbn-test/src/functional_test_runner/lib/test_metadata.ts
new file mode 100644
index 0000000000000..5789231f87044
--- /dev/null
+++ b/packages/kbn-test/src/functional_test_runner/lib/test_metadata.ts
@@ -0,0 +1,41 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import { Lifecycle } from './lifecycle';
+
+export interface ScreenshotRecord {
+ name: string;
+ base64Png: string;
+ baselinePath?: string;
+ failurePath?: string;
+}
+
+export class TestMetadata {
+ // mocha's global types mean we can't import Mocha or it will override the global jest types..............
+ private currentRunnable?: any;
+
+ constructor(lifecycle: Lifecycle) {
+ lifecycle.beforeEachRunnable.add((runnable) => {
+ this.currentRunnable = runnable;
+ });
+ }
+
+ addScreenshot(screenshot: ScreenshotRecord) {
+ this.currentRunnable._screenshots = (this.currentRunnable._screenshots || []).concat(
+ screenshot
+ );
+ }
+
+ getScreenshots(test: any): ScreenshotRecord[] {
+ if (!test || typeof test !== 'object' || !test._screenshots) {
+ return [];
+ }
+
+ return test._screenshots.slice();
+ }
+}
diff --git a/packages/kbn-test/src/functional_test_runner/public_types.ts b/packages/kbn-test/src/functional_test_runner/public_types.ts
index 6cb6d5adf4b19..426fdda74d313 100644
--- a/packages/kbn-test/src/functional_test_runner/public_types.ts
+++ b/packages/kbn-test/src/functional_test_runner/public_types.ts
@@ -8,10 +8,10 @@
import type { ToolingLog } from '@kbn/dev-utils';
-import type { Config, Lifecycle, FailureMetadata, DockerServersService, EsVersion } from './lib';
+import type { Config, Lifecycle, TestMetadata, DockerServersService, EsVersion } from './lib';
import type { Test, Suite } from './fake_mocha_types';
-export { Lifecycle, Config, FailureMetadata };
+export { Lifecycle, Config, TestMetadata };
export interface AsyncInstance {
/**
@@ -57,7 +57,7 @@ export interface GenericFtrProviderContext<
* @param serviceName
*/
hasService(
- serviceName: 'config' | 'log' | 'lifecycle' | 'failureMetadata' | 'dockerServers' | 'esVersion'
+ serviceName: 'config' | 'log' | 'lifecycle' | 'testMetadata' | 'dockerServers' | 'esVersion'
): true;
hasService(serviceName: K): serviceName is K;
hasService(serviceName: string): serviceName is Extract;
@@ -71,7 +71,7 @@ export interface GenericFtrProviderContext<
getService(serviceName: 'log'): ToolingLog;
getService(serviceName: 'lifecycle'): Lifecycle;
getService(serviceName: 'dockerServers'): DockerServersService;
- getService(serviceName: 'failureMetadata'): FailureMetadata;
+ getService(serviceName: 'testMetadata'): TestMetadata;
getService(serviceName: 'esVersion'): EsVersion;
getService(serviceName: T): ServiceMap[T];
diff --git a/packages/kbn-test/src/jest/ci_stats_jest_reporter.ts b/packages/kbn-test/src/jest/ci_stats_jest_reporter.ts
new file mode 100644
index 0000000000000..94675d87a3a24
--- /dev/null
+++ b/packages/kbn-test/src/jest/ci_stats_jest_reporter.ts
@@ -0,0 +1,120 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0 and the Server Side Public License, v 1; you may not use this file except
+ * in compliance with, at your election, the Elastic License 2.0 or the Server
+ * Side Public License, v 1.
+ */
+
+import * as Path from 'path';
+
+import getopts from 'getopts';
+import { CiStatsReporter, ToolingLog, CiStatsReportTestsOptions } from '@kbn/dev-utils';
+import type { Config } from '@jest/types';
+import { BaseReporter, Test, TestResult } from '@jest/reporters';
+import { ConsoleBuffer } from '@jest/console';
+
+type LogEntry = ConsoleBuffer[0];
+
+interface ReporterOptions {
+ testGroupType: string;
+}
+
+function formatConsoleLine({ type, message, origin }: LogEntry) {
+ const originLines = origin.split('\n');
+
+ return `console.${type}: ${message}${originLines[0] ? `\n ${originLines[0]}` : ''}`;
+}
+
+/**
+ * Jest reporter that reports tests to CI Stats
+ * @class JestJUnitReporter
+ */
+
+// eslint-disable-next-line import/no-default-export
+export default class CiStatsJestReporter extends BaseReporter {
+ private reporter: CiStatsReporter | undefined;
+ private readonly testGroupType: string;
+ private readonly reportName: string;
+ private readonly rootDir: string;
+ private startTime: number | undefined;
+
+ private group: CiStatsReportTestsOptions['group'] | undefined;
+ private readonly testRuns: CiStatsReportTestsOptions['testRuns'] = [];
+
+ constructor(config: Config.GlobalConfig, options: ReporterOptions) {
+ super();
+
+ this.rootDir = config.rootDir;
+ this.testGroupType = options?.testGroupType;
+ if (!this.testGroupType) {
+ throw new Error('missing testGroupType reporter option');
+ }
+
+ const configArg = getopts(process.argv).config;
+ if (typeof configArg !== 'string') {
+ throw new Error('expected to find a single --config arg');
+ }
+ this.reportName = configArg;
+ }
+
+ async onRunStart() {
+ const reporter = CiStatsReporter.fromEnv(
+ new ToolingLog({
+ level: 'info',
+ writeTo: process.stdout,
+ })
+ );
+
+ if (!reporter.hasBuildConfig()) {
+ return;
+ }
+
+ this.startTime = Date.now();
+ this.reporter = reporter;
+ this.group = {
+ name: this.reportName,
+ type: this.testGroupType,
+ startTime: new Date(this.startTime).toJSON(),
+ meta: {},
+ durationMs: 0,
+ };
+ }
+
+ async onTestFileResult(_: Test, testResult: TestResult) {
+ if (!this.reporter || !this.group) {
+ return;
+ }
+
+ let elapsedTime = 0;
+ for (const t of testResult.testResults) {
+ const startTime = new Date(testResult.perfStats.start + elapsedTime).toJSON();
+ elapsedTime += t.duration ?? 0;
+ this.testRuns.push({
+ startTime,
+ durationMs: t.duration ?? 0,
+ seq: this.testRuns.length + 1,
+ file: Path.relative(this.rootDir, testResult.testFilePath),
+ name: t.title,
+ result: t.status === 'failed' ? 'fail' : t.status === 'passed' ? 'pass' : 'skip',
+ suites: t.ancestorTitles,
+ type: 'test',
+ error: t.failureMessages.join('\n\n'),
+ stdout: testResult.console?.map(formatConsoleLine).join('\n'),
+ });
+ }
+ }
+
+ async onRunComplete() {
+ if (!this.reporter || !this.group || !this.testRuns.length || !this.startTime) {
+ return;
+ }
+
+ this.group.durationMs = Date.now() - this.startTime;
+
+ await this.reporter.reportTests({
+ group: this.group,
+ testRuns: this.testRuns,
+ });
+ }
+}
diff --git a/packages/kbn-test/src/mocha/index.ts b/packages/kbn-test/src/mocha/index.ts
index 4ada51c7ae013..1be65d60a9842 100644
--- a/packages/kbn-test/src/mocha/index.ts
+++ b/packages/kbn-test/src/mocha/index.ts
@@ -11,7 +11,7 @@
export { setupJUnitReportGeneration } from './junit_report_generation';
// @ts-ignore not typed yet
// @internal
-export { recordLog, snapshotLogsForRunnable } from './log_cache';
+export { recordLog, snapshotLogsForRunnable, getSnapshotOfRunnableLogs } from './log_cache';
// @ts-ignore not typed yet
// @internal
export { escapeCdata } from './xml';
diff --git a/src/core/server/core_usage_data/core_usage_data_service.mock.ts b/src/core/server/core_usage_data/core_usage_data_service.mock.ts
index 941ac5afacb40..d4ba6176cc78b 100644
--- a/src/core/server/core_usage_data/core_usage_data_service.mock.ts
+++ b/src/core/server/core_usage_data/core_usage_data_service.mock.ts
@@ -138,6 +138,7 @@ const createStartContractMock = () => {
alias: 'test_index',
primaryStoreSizeBytes: 1,
storeSizeBytes: 1,
+ savedObjectsDocsCount: 1,
},
],
legacyUrlAliases: {
diff --git a/src/core/server/core_usage_data/core_usage_data_service.test.ts b/src/core/server/core_usage_data/core_usage_data_service.test.ts
index 89d83cfdee2b8..bdaa8ae58a807 100644
--- a/src/core/server/core_usage_data/core_usage_data_service.test.ts
+++ b/src/core/server/core_usage_data/core_usage_data_service.test.ts
@@ -177,6 +177,11 @@ describe('CoreUsageDataService', () => {
},
],
} as any);
+ elasticsearch.client.asInternalUser.count.mockResolvedValueOnce({
+ body: {
+ count: '15',
+ },
+ } as any);
elasticsearch.client.asInternalUser.cat.indices.mockResolvedValueOnce({
body: [
{
@@ -188,6 +193,11 @@ describe('CoreUsageDataService', () => {
},
],
} as any);
+ elasticsearch.client.asInternalUser.count.mockResolvedValueOnce({
+ body: {
+ count: '10',
+ },
+ } as any);
elasticsearch.client.asInternalUser.search.mockResolvedValueOnce({
body: {
hits: { total: { value: 6 } },
@@ -343,6 +353,7 @@ describe('CoreUsageDataService', () => {
"docsCount": 10,
"docsDeleted": 10,
"primaryStoreSizeBytes": 2000,
+ "savedObjectsDocsCount": "15",
"storeSizeBytes": 1000,
},
Object {
@@ -350,6 +361,7 @@ describe('CoreUsageDataService', () => {
"docsCount": 20,
"docsDeleted": 20,
"primaryStoreSizeBytes": 4000,
+ "savedObjectsDocsCount": "10",
"storeSizeBytes": 2000,
},
],
diff --git a/src/core/server/core_usage_data/core_usage_data_service.ts b/src/core/server/core_usage_data/core_usage_data_service.ts
index 73f63d4d634df..609e7af3946fe 100644
--- a/src/core/server/core_usage_data/core_usage_data_service.ts
+++ b/src/core/server/core_usage_data/core_usage_data_service.ts
@@ -133,11 +133,11 @@ export class CoreUsageDataService implements CoreService())
.values()
- ).map((index) => {
+ ).map(async (index) => {
// The _cat/indices API returns the _index_ and doesn't return a way
// to map back from the index to the alias. So we have to make an API
- // call for every alias
- return elasticsearch.client.asInternalUser.cat
+ // call for every alias. The document count is the lucene document count.
+ const catIndicesResults = await elasticsearch.client.asInternalUser.cat
.indices({
index,
format: 'JSON',
@@ -145,6 +145,7 @@ export class CoreUsageDataService implements CoreService {
const stats = body[0];
+
return {
alias: kibanaOrTaskManagerIndex(index, this.kibanaConfig!.index),
docsCount: stats['docs.count'] ? parseInt(stats['docs.count'], 10) : 0,
@@ -155,6 +156,27 @@ export class CoreUsageDataService implements CoreService/_count API to get the number of saved objects
+ // to monitor if the cluster will hit the scalling limit of saved object migrations
+ const savedObjectsCounts = await elasticsearch.client.asInternalUser
+ .count({
+ index,
+ })
+ .then(({ body }) => {
+ return {
+ savedObjectsDocsCount: body.count ? body.count : 0,
+ };
+ });
+ this.logger.debug(
+ `Lucene documents count ${catIndicesResults.docsCount} from index ${catIndicesResults.alias}`
+ );
+ this.logger.debug(
+ `Saved objects documents count ${savedObjectsCounts.savedObjectsDocsCount} from index ${catIndicesResults.alias}`
+ );
+ return {
+ ...catIndicesResults,
+ ...savedObjectsCounts,
+ };
})
);
}
diff --git a/src/core/server/core_usage_data/types.ts b/src/core/server/core_usage_data/types.ts
index 7d0e9fd362d29..17eade436551d 100644
--- a/src/core/server/core_usage_data/types.ts
+++ b/src/core/server/core_usage_data/types.ts
@@ -177,6 +177,7 @@ export interface CoreServicesUsageData {
docsDeleted: number;
storeSizeBytes: number;
primaryStoreSizeBytes: number;
+ savedObjectsDocsCount: number;
}[];
legacyUrlAliases: {
activeCount: number;
diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts
index 96b3bdcad1d2e..5108418593623 100644
--- a/src/core/server/elasticsearch/elasticsearch_config.ts
+++ b/src/core/server/elasticsearch/elasticsearch_config.ts
@@ -165,10 +165,11 @@ const deprecations: ConfigDeprecationProvider = () => [
manualSteps: [
i18n.translate('core.deprecations.elasticsearchUsername.manualSteps1', {
defaultMessage:
- 'Use the elasticsearch-service-tokens CLI tool to create a new service account token for the "elastic/kibana" service account.',
+ 'Use Kibana Dev Tools to create a service account token using the API: "POST /_security/service/elastic/kibana/credential/token"',
}),
i18n.translate('core.deprecations.elasticsearchUsername.manualSteps2', {
- defaultMessage: 'Add the "elasticsearch.serviceAccountToken" setting to kibana.yml.',
+ defaultMessage:
+ 'Copy the returned token.value and add it as the "elasticsearch.serviceAccountToken" setting to kibana.yml.',
}),
i18n.translate('core.deprecations.elasticsearchUsername.manualSteps3', {
defaultMessage:
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 00a1cbc44061d..75ec18a04f54e 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -416,6 +416,7 @@ export interface CoreServicesUsageData {
docsDeleted: number;
storeSizeBytes: number;
primaryStoreSizeBytes: number;
+ savedObjectsDocsCount: number;
}[];
legacyUrlAliases: {
activeCount: number;
diff --git a/src/plugins/data/common/search/search_source/search_source.test.ts b/src/plugins/data/common/search/search_source/search_source.test.ts
index 8ef457ac586c5..72869e2555d8a 100644
--- a/src/plugins/data/common/search/search_source/search_source.test.ts
+++ b/src/plugins/data/common/search/search_source/search_source.test.ts
@@ -157,6 +157,22 @@ describe('SearchSource', () => {
expect(request.runtime_mappings).toEqual(runtimeFields);
});
+ test('except when size is 0', async () => {
+ const runtimeFields = { runtime_field: runtimeFieldDef };
+ searchSource.setField('size', 0).setField('index', {
+ ...indexPattern,
+ getComputedFields: () => ({
+ storedFields: ['hello'],
+ scriptFields: { world: {} },
+ docvalueFields: ['@timestamp'],
+ runtimeFields,
+ }),
+ } as unknown as IndexPattern);
+
+ const request = searchSource.getSearchRequestBody();
+ expect(request.fields).toBeUndefined();
+ });
+
test('never includes docvalue_fields', async () => {
searchSource.setField('index', {
...indexPattern,
@@ -211,25 +227,21 @@ describe('SearchSource', () => {
expect(request.fields).toEqual(['c', { field: 'a', format: 'date_time' }]);
});
- test('allows you to override computed fields if you provide a format', async () => {
- const indexPatternFields = indexPattern.fields;
- indexPatternFields.getByType = (type) => {
- return [];
- };
- searchSource.setField('index', {
+ test('does not allow any field info when size is 0', async () => {
+ searchSource.setField('size', 0).setField('index', {
...indexPattern,
- fields: indexPatternFields,
getComputedFields: () => ({
storedFields: [],
scriptFields: {},
- docvalueFields: [{ field: 'hello', format: 'date_time' }],
+ docvalueFields: [{ field: 'a', format: 'date_time' }],
}),
} as unknown as IndexPattern);
- searchSource.setField('fields', [{ field: 'hello', format: 'strict_date_time' }]);
+ searchSource.setField('fields', ['c']);
+ searchSource.setField('fieldsFromSource', ['a', 'b', 'd']);
const request = searchSource.getSearchRequestBody();
- expect(request).toHaveProperty('fields');
- expect(request.fields).toEqual([{ field: 'hello', format: 'strict_date_time' }]);
+ expect(request).not.toHaveProperty('_source');
+ expect(request).not.toHaveProperty('fields');
});
test('injects a date format for computed docvalue fields if none is provided', async () => {
diff --git a/src/plugins/data/common/search/search_source/search_source.ts b/src/plugins/data/common/search/search_source/search_source.ts
index 0691925ed50f7..8a7973ff08f8f 100644
--- a/src/plugins/data/common/search/search_source/search_source.ts
+++ b/src/plugins/data/common/search/search_source/search_source.ts
@@ -838,6 +838,12 @@ export class SearchSource {
body.fields = filteredDocvalueFields;
}
+ // If we aren't requesting any documents, there isn't any reason to request any field information
+ if (body.size === 0) {
+ delete body._source;
+ delete body.fields;
+ }
+
// If sorting by _score, build queries in the "must" clause instead of "filter" clause to enable scoring
const filtersInMustClause = (body.sort ?? []).some((sort: EsQuerySortValue[]) =>
sort.hasOwnProperty('_score')
diff --git a/src/plugins/data/server/search/routes/call_msearch.ts b/src/plugins/data/server/search/routes/call_msearch.ts
index 4a7db9517c688..bbcb6c2f3732e 100644
--- a/src/plugins/data/server/search/routes/call_msearch.ts
+++ b/src/plugins/data/server/search/routes/call_msearch.ts
@@ -60,8 +60,12 @@ export function getCallMsearch(dependencies: CallMsearchDependencies) {
const config = await globalConfig$.pipe(first()).toPromise();
const timeout = getShardTimeout(config);
- // trackTotalHits is not supported by msearch
- const { track_total_hits: _, ...defaultParams } = await getDefaultSearchParams(uiSettings);
+ // track_total_hits/enable_fields_emulation are not supported by msearch
+ const {
+ track_total_hits: tth,
+ enable_fields_emulation: efe,
+ ...defaultParams
+ } = await getDefaultSearchParams(uiSettings);
try {
const promise = esClient.asCurrentUser.msearch(
diff --git a/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts b/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts
index 4c75d62f12190..c2bc62c40e8de 100644
--- a/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts
+++ b/src/plugins/data/server/search/strategies/eql_search/eql_search_strategy.ts
@@ -42,7 +42,8 @@ export const eqlSearchStrategyProvider = (
const search = async () => {
const { track_total_hits: _, ...defaultParams } = await getDefaultSearchParams(
- uiSettingsClient
+ uiSettingsClient,
+ request.params?.body
);
const params = id
? getDefaultAsyncGetParams(null, options)
diff --git a/src/plugins/data/server/search/strategies/es_search/es_search_strategy.test.ts b/src/plugins/data/server/search/strategies/es_search/es_search_strategy.test.ts
index bbbc99d157fe0..aebc3dcc8404b 100644
--- a/src/plugins/data/server/search/strategies/es_search/es_search_strategy.test.ts
+++ b/src/plugins/data/server/search/strategies/es_search/es_search_strategy.test.ts
@@ -74,6 +74,7 @@ describe('ES search strategy', () => {
...params,
ignore_unavailable: true,
track_total_hits: true,
+ enable_fields_emulation: true,
});
done();
});
@@ -89,6 +90,7 @@ describe('ES search strategy', () => {
expect(mockApiCaller.mock.calls[0][0]).toEqual({
...params,
track_total_hits: true,
+ enable_fields_emulation: true,
});
done();
});
@@ -126,6 +128,7 @@ describe('ES search strategy', () => {
expect(mockApiCaller.mock.calls[0][0]).toEqual({
...params,
track_total_hits: true,
+ enable_fields_emulation: true,
});
expect(mockedApiCaller.abort).toBeCalled();
});
diff --git a/src/plugins/data/server/search/strategies/es_search/es_search_strategy.ts b/src/plugins/data/server/search/strategies/es_search/es_search_strategy.ts
index c24aa37082bd8..d508a91dae6fe 100644
--- a/src/plugins/data/server/search/strategies/es_search/es_search_strategy.ts
+++ b/src/plugins/data/server/search/strategies/es_search/es_search_strategy.ts
@@ -39,7 +39,7 @@ export const esSearchStrategyProvider = (
try {
const config = await config$.pipe(first()).toPromise();
const params = {
- ...(await getDefaultSearchParams(uiSettingsClient)),
+ ...(await getDefaultSearchParams(uiSettingsClient, request.params?.body)),
...getShardTimeout(config),
...request.params,
};
diff --git a/src/plugins/data/server/search/strategies/es_search/request_utils.ts b/src/plugins/data/server/search/strategies/es_search/request_utils.ts
index 15cad34065ddc..2123b3dba5247 100644
--- a/src/plugins/data/server/search/strategies/es_search/request_utils.ts
+++ b/src/plugins/data/server/search/strategies/es_search/request_utils.ts
@@ -7,6 +7,7 @@
*/
import type { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport';
+import { SearchRequest } from '@elastic/elasticsearch/api/types';
import type { Search } from '@elastic/elasticsearch/api/requestParams';
import type { IUiSettingsClient, SharedGlobalConfig } from 'kibana/server';
import { UI_SETTINGS } from '../../../../common';
@@ -17,18 +18,29 @@ export function getShardTimeout(config: SharedGlobalConfig): Pick
+ uiSettingsClient: Pick,
+ body: SearchRequest['body'] = {}
): Promise<
- Pick
+ Pick & {
+ enable_fields_emulation: boolean;
+ }
> {
const maxConcurrentShardRequests = await uiSettingsClient.get(
UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS
);
+
+ // Specifying specific fields from both "_source" and "fields' while emulating the fields API will throw errors in ES
+ // See https://github.com/elastic/elasticsearch/pull/75745
+ const hasFields = Array.isArray(body?.fields) && body?.fields.length > 0;
+ const hasSourceFields = body?.hasOwnProperty('_source') && typeof body?._source !== 'boolean';
+ const enableFieldsEmulation = !(hasFields && hasSourceFields);
+
return {
max_concurrent_shard_requests:
maxConcurrentShardRequests > 0 ? maxConcurrentShardRequests : undefined,
ignore_unavailable: true, // Don't fail if the index/indices don't exist
track_total_hits: true,
+ enable_fields_emulation: enableFieldsEmulation,
};
}
diff --git a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts
index 75a4ddf051418..029136384fe81 100644
--- a/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts
+++ b/src/plugins/data/server/search/strategies/ese_search/ese_search_strategy.ts
@@ -64,6 +64,7 @@ export const enhancedEsSearchStrategyProvider = (
...(await getDefaultAsyncSubmitParams(
uiSettingsClient,
searchSessionsClient.getConfig(),
+ request.params?.body,
options
)),
...request.params,
@@ -110,7 +111,7 @@ export const enhancedEsSearchStrategyProvider = (
const querystring = {
...getShardTimeout(legacyConfig),
...(await getIgnoreThrottled(uiSettingsClient)),
- ...(await getDefaultSearchParams(uiSettingsClient)),
+ ...(await getDefaultSearchParams(uiSettingsClient, request.params?.body)),
...params,
};
diff --git a/src/plugins/data/server/search/strategies/ese_search/request_utils.test.ts b/src/plugins/data/server/search/strategies/ese_search/request_utils.test.ts
index 91b323de7c07b..61c8b61f7b434 100644
--- a/src/plugins/data/server/search/strategies/ese_search/request_utils.test.ts
+++ b/src/plugins/data/server/search/strategies/ese_search/request_utils.test.ts
@@ -56,7 +56,7 @@ describe('request utils', () => {
const mockConfig = getMockSearchSessionsConfig({
defaultExpiration: moment.duration(3, 'd'),
});
- const params = await getDefaultAsyncSubmitParams(mockUiSettingsClient, mockConfig, {});
+ const params = await getDefaultAsyncSubmitParams(mockUiSettingsClient, mockConfig, {}, {});
expect(params).toHaveProperty('keep_alive', '1m');
});
@@ -67,9 +67,14 @@ describe('request utils', () => {
const mockConfig = getMockSearchSessionsConfig({
defaultExpiration: moment.duration(3, 'd'),
});
- const params = await getDefaultAsyncSubmitParams(mockUiSettingsClient, mockConfig, {
- sessionId: 'foo',
- });
+ const params = await getDefaultAsyncSubmitParams(
+ mockUiSettingsClient,
+ mockConfig,
+ {},
+ {
+ sessionId: 'foo',
+ }
+ );
expect(params).toHaveProperty('keep_alive', '259200000ms');
});
@@ -81,9 +86,14 @@ describe('request utils', () => {
defaultExpiration: moment.duration(3, 'd'),
enabled: false,
});
- const params = await getDefaultAsyncSubmitParams(mockUiSettingsClient, mockConfig, {
- sessionId: 'foo',
- });
+ const params = await getDefaultAsyncSubmitParams(
+ mockUiSettingsClient,
+ mockConfig,
+ {},
+ {
+ sessionId: 'foo',
+ }
+ );
expect(params).toHaveProperty('keep_alive', '1m');
});
@@ -92,9 +102,14 @@ describe('request utils', () => {
[UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: false,
});
const mockConfig = getMockSearchSessionsConfig({});
- const params = await getDefaultAsyncSubmitParams(mockUiSettingsClient, mockConfig, {
- sessionId: 'foo',
- });
+ const params = await getDefaultAsyncSubmitParams(
+ mockUiSettingsClient,
+ mockConfig,
+ {},
+ {
+ sessionId: 'foo',
+ }
+ );
expect(params).toHaveProperty('keep_on_completion', true);
});
@@ -106,11 +121,45 @@ describe('request utils', () => {
defaultExpiration: moment.duration(3, 'd'),
enabled: false,
});
- const params = await getDefaultAsyncSubmitParams(mockUiSettingsClient, mockConfig, {
- sessionId: 'foo',
- });
+ const params = await getDefaultAsyncSubmitParams(
+ mockUiSettingsClient,
+ mockConfig,
+ {},
+ {
+ sessionId: 'foo',
+ }
+ );
expect(params).toHaveProperty('keep_on_completion', false);
});
+
+ test('Sends `enable_fields_emulation: true` for BWC with CCS if not specifying both fields and _source', async () => {
+ const mockUiSettingsClient = getMockUiSettingsClient({
+ [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: false,
+ });
+ const mockConfig = getMockSearchSessionsConfig({
+ defaultExpiration: moment.duration(3, 'd'),
+ enabled: false,
+ });
+ const params = await getDefaultAsyncSubmitParams(mockUiSettingsClient, mockConfig, {}, {});
+ expect(params).toHaveProperty('enable_fields_emulation', true);
+ });
+
+ test('Sends `enable_fields_emulation: false` if specifying both fields and _source', async () => {
+ const mockUiSettingsClient = getMockUiSettingsClient({
+ [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: false,
+ });
+ const mockConfig = getMockSearchSessionsConfig({
+ defaultExpiration: moment.duration(3, 'd'),
+ enabled: false,
+ });
+ const params = await getDefaultAsyncSubmitParams(
+ mockUiSettingsClient,
+ mockConfig,
+ { fields: ['foo'], _source: { excludes: ['bar'] } },
+ {}
+ );
+ expect(params).toHaveProperty('enable_fields_emulation', false);
+ });
});
describe('getDefaultAsyncGetParams', () => {
diff --git a/src/plugins/data/server/search/strategies/ese_search/request_utils.ts b/src/plugins/data/server/search/strategies/ese_search/request_utils.ts
index f8fb54cfd870b..589f502047d54 100644
--- a/src/plugins/data/server/search/strategies/ese_search/request_utils.ts
+++ b/src/plugins/data/server/search/strategies/ese_search/request_utils.ts
@@ -12,6 +12,7 @@ import {
AsyncSearchSubmit,
Search,
} from '@elastic/elasticsearch/api/requestParams';
+import { SearchRequest } from '@elastic/elasticsearch/api/types';
import { ISearchOptions, UI_SETTINGS } from '../../../../common';
import { getDefaultSearchParams } from '../es_search';
import { SearchSessionsConfigSchema } from '../../../../config';
@@ -32,7 +33,8 @@ export async function getIgnoreThrottled(
export async function getDefaultAsyncSubmitParams(
uiSettingsClient: Pick,
searchSessionsConfig: SearchSessionsConfigSchema | null,
- options: ISearchOptions
+ body: SearchRequest['body'] = {},
+ options: ISearchOptions = {}
): Promise<
Pick<
AsyncSearchSubmit,
@@ -44,7 +46,9 @@ export async function getDefaultAsyncSubmitParams(
| 'ignore_unavailable'
| 'track_total_hits'
| 'keep_on_completion'
- >
+ > & {
+ enable_fields_emulation: boolean;
+ }
> {
const useSearchSessions = searchSessionsConfig?.enabled && !!options.sessionId;
@@ -64,8 +68,7 @@ export async function getDefaultAsyncSubmitParams(
// The initial keepalive is as defined in defaultExpiration if search sessions are used or 1m otherwise.
keep_alive: keepAlive,
...(await getIgnoreThrottled(uiSettingsClient)),
- ...(await getDefaultSearchParams(uiSettingsClient)),
- // If search sessions are used, set the initial expiration time.
+ ...(await getDefaultSearchParams(uiSettingsClient, body)),
};
}
diff --git a/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts
index b1cf0ecd2213e..a208832baf719 100644
--- a/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts
+++ b/src/plugins/kibana_usage_collection/server/collectors/core/core_usage_collector.ts
@@ -355,14 +355,14 @@ export function getCoreUsageCollector(
type: 'long',
_meta: {
description:
- 'The number of documents in the index, including hidden nested documents.',
+ 'The number of lucene documents in the index, including hidden nested documents.',
},
},
docsDeleted: {
type: 'long',
_meta: {
description:
- 'The number of deleted documents in the index, including hidden nested documents.',
+ 'The number of deleted lucene documents in the index, including hidden nested documents.',
},
},
alias: {
@@ -382,6 +382,12 @@ export function getCoreUsageCollector(
description: 'The size in bytes of the index, for primaries and replicas.',
},
},
+ savedObjectsDocsCount: {
+ type: 'long',
+ _meta: {
+ description: 'The number of saved objects documents in the index.',
+ },
+ },
},
},
legacyUrlAliases: {
diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json
index d78adb7e22958..b1d11acb0b225 100644
--- a/src/plugins/telemetry/schema/oss_plugins.json
+++ b/src/plugins/telemetry/schema/oss_plugins.json
@@ -6248,13 +6248,13 @@
"docsCount": {
"type": "long",
"_meta": {
- "description": "The number of documents in the index, including hidden nested documents."
+ "description": "The number of lucene documents in the index, including hidden nested documents."
}
},
"docsDeleted": {
"type": "long",
"_meta": {
- "description": "The number of deleted documents in the index, including hidden nested documents."
+ "description": "The number of deleted lucene documents in the index, including hidden nested documents."
}
},
"alias": {
@@ -6274,6 +6274,12 @@
"_meta": {
"description": "The size in bytes of the index, for primaries and replicas."
}
+ },
+ "savedObjectsDocsCount": {
+ "type": "long",
+ "_meta": {
+ "description": "The number of saved objects documents in the index."
+ }
}
}
}
diff --git a/test/functional/services/common/screenshots.ts b/test/functional/services/common/screenshots.ts
index 0f2ab8e6edfbe..d5f901300941f 100644
--- a/test/functional/services/common/screenshots.ts
+++ b/test/functional/services/common/screenshots.ts
@@ -22,7 +22,7 @@ const writeFileAsync = promisify(writeFile);
export class ScreenshotsService extends FtrService {
private readonly log = this.ctx.getService('log');
private readonly config = this.ctx.getService('config');
- private readonly failureMetadata = this.ctx.getService('failureMetadata');
+ private readonly testMetadata = this.ctx.getService('testMetadata');
private readonly browser = this.ctx.getService('browser');
private readonly SESSION_DIRECTORY = resolve(this.config.get('screenshots.directory'), 'session');
@@ -51,11 +51,17 @@ export class ScreenshotsService extends FtrService {
async compareAgainstBaseline(name: string, updateBaselines: boolean, el?: WebElementWrapper) {
this.log.debug('compareAgainstBaseline');
const sessionPath = resolve(this.SESSION_DIRECTORY, `${name}.png`);
- await this.capture(sessionPath, el);
-
const baselinePath = resolve(this.BASELINE_DIRECTORY, `${name}.png`);
const failurePath = resolve(this.FAILURE_DIRECTORY, `${name}.png`);
+ await this.capture({
+ path: sessionPath,
+ name,
+ el,
+ baselinePath,
+ failurePath,
+ });
+
if (updateBaselines) {
this.log.debug('Updating baseline snapshot');
// Make the directory if it doesn't exist
@@ -76,22 +82,42 @@ export class ScreenshotsService extends FtrService {
async take(name: string, el?: WebElementWrapper, subDirectories: string[] = []) {
const path = resolve(this.SESSION_DIRECTORY, ...subDirectories, `${name}.png`);
- await this.capture(path, el);
- this.failureMetadata.addScreenshot(name, path);
+ await this.capture({ path, name, el });
}
async takeForFailure(name: string, el?: WebElementWrapper) {
const path = resolve(this.FAILURE_DIRECTORY, `${name}.png`);
- await this.capture(path, el);
- this.failureMetadata.addScreenshot(`failure[${name}]`, path);
+ await this.capture({
+ path,
+ name: `failure[${name}]`,
+ el,
+ });
}
- private async capture(path: string, el?: WebElementWrapper) {
+ private async capture({
+ path,
+ el,
+ name,
+ baselinePath,
+ failurePath,
+ }: {
+ path: string;
+ name: string;
+ el?: WebElementWrapper;
+ baselinePath?: string;
+ failurePath?: string;
+ }) {
try {
this.log.info(`Taking screenshot "${path}"`);
const screenshot = await (el ? el.takeScreenshot() : this.browser.takeScreenshot());
await mkdirAsync(dirname(path), { recursive: true });
await writeFileAsync(path, screenshot, 'base64');
+ this.testMetadata.addScreenshot({
+ name,
+ base64Png: Buffer.isBuffer(screenshot) ? screenshot.toString('base64') : screenshot,
+ baselinePath,
+ failurePath,
+ });
} catch (err) {
this.log.error('SCREENSHOT FAILED');
this.log.error(err);
diff --git a/x-pack/plugins/cases/public/components/markdown_editor/renderer.tsx b/x-pack/plugins/cases/public/components/markdown_editor/renderer.tsx
index 6a91dda97a892..ae08d39a53e7a 100644
--- a/x-pack/plugins/cases/public/components/markdown_editor/renderer.tsx
+++ b/x-pack/plugins/cases/public/components/markdown_editor/renderer.tsx
@@ -31,6 +31,7 @@ const MarkdownRendererComponent: React.FC = ({ children, disableLinks })
{children}
diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_markdown.tsx b/x-pack/plugins/cases/public/components/user_action_tree/user_action_markdown.tsx
index f7a6932b35856..24fac6f849d58 100644
--- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_markdown.tsx
+++ b/x-pack/plugins/cases/public/components/user_action_tree/user_action_markdown.tsx
@@ -112,7 +112,7 @@ export const UserActionMarkdown = forwardRef
) : (
-
+
{content}
);
diff --git a/x-pack/plugins/file_upload/server/analyze_file.tsx b/x-pack/plugins/file_upload/server/analyze_file.tsx
index 2239697083492..a2a5f3835f910 100644
--- a/x-pack/plugins/file_upload/server/analyze_file.tsx
+++ b/x-pack/plugins/file_upload/server/analyze_file.tsx
@@ -14,10 +14,13 @@ export async function analyzeFile(
overrides: InputOverrides
): Promise {
overrides.explain = overrides.explain === undefined ? 'true' : overrides.explain;
- const { body } = await client.asInternalUser.textStructure.findStructure({
- body: data,
- ...overrides,
- });
+ const { body } = await client.asInternalUser.textStructure.findStructure(
+ {
+ body: data,
+ ...overrides,
+ },
+ { maxRetries: 0 }
+ );
const { hasOverrides, reducedOverrides } = formatOverrides(overrides);
diff --git a/x-pack/plugins/file_upload/server/import_data.ts b/x-pack/plugins/file_upload/server/import_data.ts
index deb170974ced8..af2e05d8af196 100644
--- a/x-pack/plugins/file_upload/server/import_data.ts
+++ b/x-pack/plugins/file_upload/server/import_data.ts
@@ -103,7 +103,7 @@ export function importDataProvider({ asCurrentUser }: IScopedClusterClient) {
body.settings = settings;
}
- await asCurrentUser.indices.create({ index, body });
+ await asCurrentUser.indices.create({ index, body }, { maxRetries: 0 });
}
async function indexData(index: string, pipelineId: string, data: InputData) {
@@ -119,7 +119,7 @@ export function importDataProvider({ asCurrentUser }: IScopedClusterClient) {
settings.pipeline = pipelineId;
}
- const { body: resp } = await asCurrentUser.bulk(settings);
+ const { body: resp } = await asCurrentUser.bulk(settings, { maxRetries: 0 });
if (resp.errors) {
throw resp;
} else {
diff --git a/x-pack/plugins/fleet/server/integration_tests/docker_registry_helper.ts b/x-pack/plugins/fleet/server/integration_tests/docker_registry_helper.ts
new file mode 100644
index 0000000000000..bb34dc3258d05
--- /dev/null
+++ b/x-pack/plugins/fleet/server/integration_tests/docker_registry_helper.ts
@@ -0,0 +1,69 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * 2.0; you may not use this file except in compliance with the Elastic License
+ * 2.0.
+ */
+
+import { spawn } from 'child_process';
+import type { ChildProcess } from 'child_process';
+
+import fetch from 'node-fetch';
+
+const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
+
+export function useDockerRegistry() {
+ const packageRegistryPort = process.env.FLEET_PACKAGE_REGISTRY_PORT || '8081';
+
+ if (!packageRegistryPort.match(/^[0-9]{4}/)) {
+ throw new Error('Invalid FLEET_PACKAGE_REGISTRY_PORT');
+ }
+
+ let dockerProcess: ChildProcess | undefined;
+ async function startDockerRegistryServer() {
+ const dockerImage = `docker.elastic.co/package-registry/distribution@sha256:de952debe048d903fc73e8a4472bb48bb95028d440cba852f21b863d47020c61`;
+
+ const args = ['run', '--rm', '-p', `${packageRegistryPort}:8080`, dockerImage];
+
+ dockerProcess = spawn('docker', args, { stdio: 'inherit' });
+
+ let isExited = dockerProcess.exitCode !== null;
+ dockerProcess.once('exit', () => {
+ isExited = true;
+ });
+
+ let retries = 0;
+ while (!isExited && retries++ <= 20) {
+ try {
+ const res = await fetch(`http://localhost:${packageRegistryPort}/`);
+ if (res.status === 200) {
+ return;
+ }
+ } catch (err) {
+ // swallow errors
+ }
+
+ await delay(3000);
+ }
+
+ dockerProcess.kill();
+ throw new Error('Unable to setup docker registry');
+ }
+
+ async function cleanupDockerRegistryServer() {
+ if (dockerProcess && !dockerProcess.killed) {
+ dockerProcess.kill();
+ }
+ }
+
+ beforeAll(async () => {
+ jest.setTimeout(5 * 60 * 1000); // 5 minutes timeout
+ await startDockerRegistryServer();
+ });
+
+ afterAll(async () => {
+ await cleanupDockerRegistryServer();
+ });
+
+ return `http://localhost:${packageRegistryPort}`;
+}
diff --git a/x-pack/plugins/fleet/server/integration_tests/reset_preconfiguration.test.ts b/x-pack/plugins/fleet/server/integration_tests/reset_preconfiguration.test.ts
index 042341212bd13..87b47a915b19f 100644
--- a/x-pack/plugins/fleet/server/integration_tests/reset_preconfiguration.test.ts
+++ b/x-pack/plugins/fleet/server/integration_tests/reset_preconfiguration.test.ts
@@ -11,6 +11,8 @@ import * as kbnTestServer from 'src/core/test_helpers/kbn_server';
import type { AgentPolicySOAttributes } from '../types';
+import { useDockerRegistry } from './docker_registry_helper';
+
const logFilePath = Path.join(__dirname, 'logs.log');
type Root = ReturnType;
@@ -36,6 +38,8 @@ describe.skip('Fleet preconfiguration rest', () => {
let esServer: kbnTestServer.TestElasticsearchUtils;
let kbnServer: kbnTestServer.TestKibanaUtils;
+ const registryUrl = useDockerRegistry();
+
const startServers = async () => {
const { startES } = kbnTestServer.createTestServers({
adjustTimeout: (t) => jest.setTimeout(t),
@@ -53,6 +57,7 @@ describe.skip('Fleet preconfiguration rest', () => {
{
xpack: {
fleet: {
+ registryUrl,
// Preconfigure two policies test-12345 and test-456789
agentPolicies: [
{
diff --git a/x-pack/test/api_integration/apis/management/index_management/cluster_nodes.ts b/x-pack/test/api_integration/apis/management/index_management/cluster_nodes.ts
index 30c12bf33d763..e885b677aaffb 100644
--- a/x-pack/test/api_integration/apis/management/index_management/cluster_nodes.ts
+++ b/x-pack/test/api_integration/apis/management/index_management/cluster_nodes.ts
@@ -19,7 +19,7 @@ export default function ({ getService }: FtrProviderContext) {
it('should fetch the nodes plugins', async () => {
const { body } = await getNodesPlugins().expect(200);
- expect(body).eql([]);
+ expect(Array.isArray(body)).to.be(true);
});
});
}
diff --git a/x-pack/test/functional/apps/maps/mvt_super_fine.js b/x-pack/test/functional/apps/maps/mvt_super_fine.js
index dcd2923cb9335..fd60e0a489ed6 100644
--- a/x-pack/test/functional/apps/maps/mvt_super_fine.js
+++ b/x-pack/test/functional/apps/maps/mvt_super_fine.js
@@ -34,7 +34,7 @@ export default function ({ getPageObjects, getService }) {
//Source should be correct
expect(
mapboxStyle.sources[MB_VECTOR_SOURCE_ID].tiles[0].startsWith(
- `/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=geo.coordinates&index=logstash-*&requestBody=(_source:(excludes:!()),aggs:(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:geo.coordinates)),max_of_bytes:(max:(field:bytes))),geotile_grid:(bounds:!n,field:geo.coordinates,precision:!n,shard_size:65535,size:65535))),fields:!((field:'@timestamp',format:date_time),(field:'relatedContent.article:modified_time',format:date_time),(field:'relatedContent.article:published_time',format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((range:('@timestamp':(format:strict_date_optional_time,gte:'2015-09-20T00:00:00.000Z',lte:'2015-09-20T01:00:00.000Z')))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(hour_of_day:(script:(lang:painless,source:'doc[!'@timestamp!'].value.getHour()'))),size:0,stored_fields:!('*'))&requestType=grid&geoFieldType=geo_point`
+ `/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=geo.coordinates&index=logstash-*&requestBody=(aggs:(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:geo.coordinates)),max_of_bytes:(max:(field:bytes))),geotile_grid:(bounds:!n,field:geo.coordinates,precision:!n,shard_size:65535,size:65535))),query:(bool:(filter:!((range:('@timestamp':(format:strict_date_optional_time,gte:'2015-09-20T00:00:00.000Z',lte:'2015-09-20T01:00:00.000Z')))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(hour_of_day:(script:(lang:painless,source:'doc[!'@timestamp!'].value.getHour()'))),size:0,stored_fields:!('*'))&requestType=grid&geoFieldType=geo_point`
)
).to.equal(true);
diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/advanced_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/advanced_job.ts
index 9a0bd824ccd90..1fc4c87619f18 100644
--- a/x-pack/test/functional/apps/ml/anomaly_detection/advanced_job.ts
+++ b/x-pack/test/functional/apps/ml/anomaly_detection/advanced_job.ts
@@ -433,7 +433,6 @@ export default function ({ getService }: FtrProviderContext) {
await ml.testExecution.logTestStep(
'job creation displays the created job in the job list'
);
- await ml.jobTable.refreshJobList();
await ml.jobTable.filterWithSearchString(testData.jobId, 1);
await ml.testExecution.logTestStep(
@@ -649,7 +648,6 @@ export default function ({ getService }: FtrProviderContext) {
await ml.testExecution.logTestStep(
'job cloning displays the created job in the job list'
);
- await ml.jobTable.refreshJobList();
await ml.jobTable.filterWithSearchString(testData.jobIdClone, 1);
await ml.testExecution.logTestStep(
diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/aggregated_scripted_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/aggregated_scripted_job.ts
index 332bc1f59d141..25261e4d05ca1 100644
--- a/x-pack/test/functional/apps/ml/anomaly_detection/aggregated_scripted_job.ts
+++ b/x-pack/test/functional/apps/ml/anomaly_detection/aggregated_scripted_job.ts
@@ -396,7 +396,6 @@ export default function ({ getService }: FtrProviderContext) {
await ml.testExecution.logTestStep(
'check that the single metric viewer button is enabled'
);
- await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(testData.jobConfig.job_id, 1);
await ml.jobTable.assertJobActionSingleMetricViewerButtonEnabled(
@@ -442,7 +441,6 @@ export default function ({ getService }: FtrProviderContext) {
await ml.testExecution.logTestStep(
'check that the single metric viewer button is disabled'
);
- await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(testData.jobConfig.job_id, 1);
await ml.jobTable.assertJobActionSingleMetricViewerButtonEnabled(
diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts b/x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts
index 2acb16a4cff89..f7cae920106de 100644
--- a/x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts
+++ b/x-pack/test/functional/apps/ml/anomaly_detection/annotations.ts
@@ -58,7 +58,6 @@ export default function ({ getService }: FtrProviderContext) {
await ml.navigation.navigateToMl();
await ml.navigation.navigateToJobManagement();
- await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(jobId, 1);
await ml.jobTable.clickOpenJobInSingleMetricViewerButton(jobId);
@@ -90,7 +89,6 @@ export default function ({ getService }: FtrProviderContext) {
await ml.testExecution.logTestStep('should display created annotation in job list');
await ml.navigation.navigateToJobManagement();
- await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(jobId, 1);
await ml.jobTable.openAnnotationsTab(jobId);
await ml.jobAnnotations.assertAnnotationExists({
@@ -125,7 +123,6 @@ export default function ({ getService }: FtrProviderContext) {
await ml.navigation.navigateToMl();
await ml.navigation.navigateToJobManagement();
- await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(jobId, 1);
await ml.jobTable.openAnnotationsTab(jobId);
await ml.jobAnnotations.assertAnnotationContentById(
@@ -179,7 +176,6 @@ export default function ({ getService }: FtrProviderContext) {
await ml.testExecution.logTestStep('should display edited annotation in job list');
await ml.navigation.navigateToJobManagement();
- await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(jobId, 1);
await ml.jobTable.openAnnotationsTab(jobId);
await ml.jobAnnotations.assertAnnotationContentById(annotationId, expectedEditedAnnotation);
@@ -200,7 +196,6 @@ export default function ({ getService }: FtrProviderContext) {
await ml.navigation.navigateToMl();
await ml.navigation.navigateToJobManagement();
- await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(jobId, 1);
await ml.jobTable.openAnnotationsTab(jobId);
@@ -221,7 +216,6 @@ export default function ({ getService }: FtrProviderContext) {
await ml.navigation.navigateToMl();
await ml.navigation.navigateToJobManagement();
- await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(jobId, 1);
await ml.jobTable.clickOpenJobInSingleMetricViewerButton(jobId);
@@ -257,7 +251,6 @@ export default function ({ getService }: FtrProviderContext) {
await ml.testExecution.logTestStep('does not show the deleted annotation in job list');
await ml.navigation.navigateToJobManagement();
- await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(jobId, 1);
await ml.jobTable.openAnnotationsTab(jobId);
await ml.jobAnnotations.assertAnnotationsRowMissing(annotationId);
diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts b/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts
index 341217b029d66..915ec4e3b3abf 100644
--- a/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts
+++ b/x-pack/test/functional/apps/ml/anomaly_detection/anomaly_explorer.ts
@@ -102,7 +102,6 @@ export default function ({ getService }: FtrProviderContext) {
await ml.navigation.navigateToJobManagement();
await ml.testExecution.logTestStep('open job in anomaly explorer');
- await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(testData.jobConfig.job_id, 1);
await ml.jobTable.clickOpenJobInAnomalyExplorerButton(testData.jobConfig.job_id);
diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts
index 7c07acd7e7185..f8360804d7e83 100644
--- a/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts
+++ b/x-pack/test/functional/apps/ml/anomaly_detection/categorization_job.ts
@@ -202,7 +202,6 @@ export default function ({ getService }: FtrProviderContext) {
await ml.navigation.navigateToMl();
await ml.navigation.navigateToJobManagement();
- await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(jobId, 1);
await ml.testExecution.logTestStep(
@@ -318,7 +317,6 @@ export default function ({ getService }: FtrProviderContext) {
await ml.navigation.navigateToMl();
await ml.navigation.navigateToJobManagement();
- await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(jobIdClone, 1);
await ml.testExecution.logTestStep(
@@ -350,7 +348,6 @@ export default function ({ getService }: FtrProviderContext) {
await ml.testExecution.logTestStep(
'job deletion does not display the deleted job in the job list any more'
);
- await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(jobIdClone, 0);
await ml.testExecution.logTestStep(
diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts
index fb60534b87aa0..0401d13780403 100644
--- a/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts
+++ b/x-pack/test/functional/apps/ml/anomaly_detection/date_nanos_job.ts
@@ -313,7 +313,6 @@ export default function ({ getService }: FtrProviderContext) {
await ml.testExecution.logTestStep(
'job creation displays the created job in the job list'
);
- await ml.jobTable.refreshJobList();
await ml.jobTable.filterWithSearchString(testData.jobId, 1);
await ml.testExecution.logTestStep(
diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/forecasts.ts b/x-pack/test/functional/apps/ml/anomaly_detection/forecasts.ts
index 19f8e508af7e0..4f97f9f48dcdb 100644
--- a/x-pack/test/functional/apps/ml/anomaly_detection/forecasts.ts
+++ b/x-pack/test/functional/apps/ml/anomaly_detection/forecasts.ts
@@ -63,7 +63,6 @@ export default function ({ getService }: FtrProviderContext) {
await ml.navigation.navigateToJobManagement();
await ml.testExecution.logTestStep('open job in single metric viewer');
- await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(JOB_CONFIG.job_id, 1);
await ml.jobTable.clickOpenJobInSingleMetricViewerButton(JOB_CONFIG.job_id);
diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts
index 9f4982338288a..4786f51bdc414 100644
--- a/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts
+++ b/x-pack/test/functional/apps/ml/anomaly_detection/multi_metric_job.ts
@@ -205,7 +205,6 @@ export default function ({ getService }: FtrProviderContext) {
await ml.navigation.navigateToMl();
await ml.navigation.navigateToJobManagement();
- await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(jobId, 1);
await ml.testExecution.logTestStep(
@@ -338,7 +337,6 @@ export default function ({ getService }: FtrProviderContext) {
await ml.navigation.navigateToMl();
await ml.navigation.navigateToJobManagement();
- await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(jobIdClone, 1);
await ml.testExecution.logTestStep(
diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts
index 4335d99bbf1af..f522f5ebefd9a 100644
--- a/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts
+++ b/x-pack/test/functional/apps/ml/anomaly_detection/population_job.ts
@@ -231,7 +231,6 @@ export default function ({ getService }: FtrProviderContext) {
await ml.navigation.navigateToMl();
await ml.navigation.navigateToJobManagement();
- await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(jobId, 1);
await ml.testExecution.logTestStep(
@@ -375,7 +374,6 @@ export default function ({ getService }: FtrProviderContext) {
await ml.navigation.navigateToMl();
await ml.navigation.navigateToJobManagement();
- await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(jobIdClone, 1);
await ml.testExecution.logTestStep(
diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts
index c5945f0d91a02..f314052035ff1 100644
--- a/x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts
+++ b/x-pack/test/functional/apps/ml/anomaly_detection/saved_search_job.ts
@@ -411,7 +411,6 @@ export default function ({ getService }: FtrProviderContext) {
await ml.navigation.navigateToMl();
await ml.navigation.navigateToJobManagement();
- await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(testData.jobId, 1);
await ml.testExecution.logTestStep(
diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts
index 35d2a01ead8f0..8464abdf511ed 100644
--- a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts
+++ b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job.ts
@@ -184,7 +184,6 @@ export default function ({ getService }: FtrProviderContext) {
await ml.navigation.navigateToMl();
await ml.navigation.navigateToJobManagement();
- await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(jobId, 1);
await ml.testExecution.logTestStep(
@@ -301,7 +300,6 @@ export default function ({ getService }: FtrProviderContext) {
await ml.navigation.navigateToMl();
await ml.navigation.navigateToJobManagement();
- await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(jobIdClone, 1);
await ml.testExecution.logTestStep(
@@ -333,7 +331,6 @@ export default function ({ getService }: FtrProviderContext) {
await ml.testExecution.logTestStep(
'job deletion does not display the deleted job in the job list any more'
);
- await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(jobIdClone, 0);
await ml.testExecution.logTestStep(
diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job_without_datafeed_start.ts b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job_without_datafeed_start.ts
index 70438069f3447..8d08fb84a8f55 100644
--- a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job_without_datafeed_start.ts
+++ b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_job_without_datafeed_start.ts
@@ -135,7 +135,6 @@ export default function ({ getService }: FtrProviderContext) {
await ml.jobWizardCommon.assertCreateJobButtonExists();
await ml.jobWizardCommon.createJobWithoutDatafeedStart();
- await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(jobId, 1);
await ml.testExecution.logTestStep(
diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_viewer.ts b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_viewer.ts
index 661b9a700ef3f..0a04201eeef81 100644
--- a/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_viewer.ts
+++ b/x-pack/test/functional/apps/ml/anomaly_detection/single_metric_viewer.ts
@@ -64,7 +64,6 @@ export default function ({ getService }: FtrProviderContext) {
await ml.navigation.navigateToJobManagement();
await ml.testExecution.logTestStep('open job in single metric viewer');
- await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(JOB_CONFIG.job_id, 1);
await ml.jobTable.clickOpenJobInSingleMetricViewerButton(JOB_CONFIG.job_id);
@@ -152,7 +151,6 @@ export default function ({ getService }: FtrProviderContext) {
await ml.navigation.navigateToJobManagement();
await ml.testExecution.logTestStep('open job in single metric viewer');
- await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(jobConfig.job_id, 1);
await ml.jobTable.clickOpenJobInSingleMetricViewerButton(jobConfig.job_id);
diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts b/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts
index 4fb1b03599577..044e318cd92de 100644
--- a/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts
+++ b/x-pack/test/functional/apps/ml/stack_management_jobs/import_jobs.ts
@@ -75,9 +75,8 @@ export default function ({ getService }: FtrProviderContext) {
it('ensures jobs have been imported', async () => {
if (testData.expected.jobType === 'anomaly-detector') {
await ml.navigation.navigateToStackManagementJobsListPageAnomalyDetectionTab();
- await ml.jobTable.refreshJobList();
for (const id of testData.expected.jobIds) {
- await ml.jobTable.filterWithSearchString(id);
+ await ml.jobTable.filterWithSearchString(id, 1);
}
for (const id of testData.expected.skippedJobIds) {
await ml.jobTable.filterWithSearchString(id, 0);
diff --git a/x-pack/test/functional/apps/ml/stack_management_jobs/manage_spaces.ts b/x-pack/test/functional/apps/ml/stack_management_jobs/manage_spaces.ts
index 3eea2ac125043..6cf704e966d38 100644
--- a/x-pack/test/functional/apps/ml/stack_management_jobs/manage_spaces.ts
+++ b/x-pack/test/functional/apps/ml/stack_management_jobs/manage_spaces.ts
@@ -174,7 +174,7 @@ export default function ({ getService }: FtrProviderContext) {
await ml.navigation.navigateToStackManagementJobsListPage();
// AD
- await ml.jobTable.filterWithSearchString(testData.adJobId);
+ await ml.jobTable.filterWithSearchString(testData.adJobId, 1);
await ml.stackManagementJobs.assertADJobRowSpaces(testData.adJobId, [
testData.initialSpace,
]);
@@ -218,7 +218,7 @@ export default function ({ getService }: FtrProviderContext) {
// AD
await ml.navigation.navigateToStackManagementJobsListPageAnomalyDetectionTab();
- await ml.jobTable.filterWithSearchString(testData.adJobId);
+ await ml.jobTable.filterWithSearchString(testData.adJobId, 1);
await ml.stackManagementJobs.assertADJobRowSpaces(testData.adJobId, expectedJobRowSpaces);
// DFA
diff --git a/x-pack/test/functional/services/ml/job_table.ts b/x-pack/test/functional/services/ml/job_table.ts
index 40ef733bc5e06..aec834702324f 100644
--- a/x-pack/test/functional/services/ml/job_table.ts
+++ b/x-pack/test/functional/services/ml/job_table.ts
@@ -239,6 +239,7 @@ export function MachineLearningJobTableProvider(
public async filterWithSearchString(filter: string, expectedRowCount: number = 1) {
await this.waitForJobsToLoad();
+ await this.refreshJobList();
const searchBar = await testSubjects.find('mlJobListSearchBar');
const searchBarInput = await searchBar.findByTagName('input');
await searchBarInput.clearValueWithKeyboard();
diff --git a/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/custom_urls.ts b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/custom_urls.ts
index a823bff61dbdf..6906fa5dfc38a 100644
--- a/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/custom_urls.ts
+++ b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/custom_urls.ts
@@ -86,7 +86,6 @@ export default function ({ getService }: FtrProviderContext) {
await ml.navigation.navigateToJobManagement();
await ml.testExecution.logTestStep('open job in anomaly explorer');
- await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(ecommerceJobConfig.job_id, 1);
await ml.jobTable.clickOpenJobInAnomalyExplorerButton(ecommerceJobConfig.job_id);
await ml.commonUI.waitForMlLoadingIndicatorToDisappear();
diff --git a/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/geographic_data.ts b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/geographic_data.ts
index 24a92612a7595..868e649950ba5 100644
--- a/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/geographic_data.ts
+++ b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/geographic_data.ts
@@ -229,7 +229,6 @@ export default function ({ getPageObject, getService }: FtrProviderContext) {
await elasticChart.setNewChartUiDebugFlag(true);
await ml.testExecution.logTestStep('open job in anomaly explorer');
- await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(ecommerceGeoJobConfig.job_id, 1);
await ml.jobTable.clickOpenJobInAnomalyExplorerButton(ecommerceGeoJobConfig.job_id);
await ml.commonUI.waitForMlLoadingIndicatorToDisappear();
@@ -260,7 +259,6 @@ export default function ({ getPageObject, getService }: FtrProviderContext) {
await elasticChart.setNewChartUiDebugFlag(true);
await ml.testExecution.logTestStep('open job in anomaly explorer');
- await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(weblogGeoJobConfig.job_id, 1);
await ml.jobTable.clickOpenJobInAnomalyExplorerButton(weblogGeoJobConfig.job_id);
await ml.commonUI.waitForMlLoadingIndicatorToDisappear();
diff --git a/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/mapping_anomalies.ts b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/mapping_anomalies.ts
index 6d653e0aed43e..e1859c5ae0a76 100644
--- a/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/mapping_anomalies.ts
+++ b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/mapping_anomalies.ts
@@ -114,7 +114,6 @@ export default function ({ getPageObject, getService }: FtrProviderContext) {
await ml.navigation.navigateToJobManagement();
await ml.testExecution.logTestStep('open job in anomaly explorer');
- await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(weblogVectorJobConfig.job_id, 1);
await ml.jobTable.clickOpenJobInAnomalyExplorerButton(weblogVectorJobConfig.job_id);
await ml.commonUI.waitForMlLoadingIndicatorToDisappear();
diff --git a/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/population_analysis.ts b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/population_analysis.ts
index 235b78ca8a662..23ef7ce03ce09 100644
--- a/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/population_analysis.ts
+++ b/x-pack/test/screenshot_creation/apps/ml_docs/anomaly_detection/population_analysis.ts
@@ -82,7 +82,6 @@ export default function ({ getService }: FtrProviderContext) {
await elasticChart.setNewChartUiDebugFlag(true);
await ml.testExecution.logTestStep('open job in anomaly explorer');
- await ml.jobTable.waitForJobsToLoad();
await ml.jobTable.filterWithSearchString(populationJobConfig.job_id, 1);
await ml.jobTable.clickOpenJobInAnomalyExplorerButton(populationJobConfig.job_id);
await ml.commonUI.waitForMlLoadingIndicatorToDisappear();