diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b87969f0..fc8d81f6f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Added response schema for `PUT` and `DELETE /_plugins/_transform/{id}` ([#722](https://github.com/opensearch-project/opensearch-api-specification/pull/716)) - Added response schema for `GET /_plugins/_knn/warmup/{index}` ([#717](https://github.com/opensearch-project/opensearch-api-specification/pull/717)) - Added support for multiple test verbs ([#724](https://github.com/opensearch-project/opensearch-api-specification/pull/724)) +- Added support for combining output variables ([#737](https://github.com/opensearch-project/opensearch-api-specification/pull/737)) ### Removed - Removed unsupported `_common.mapping:SourceField`'s `mode` field and associated `_common.mapping:SourceFieldMode` enum ([#652](https://github.com/opensearch-project/opensearch-api-specification/pull/652)) diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md index 362f6359f..52b584982 100644 --- a/TESTING_GUIDE.md +++ b/TESTING_GUIDE.md @@ -230,7 +230,9 @@ Consider the following chapters in [ml/model_groups](tests/plugins/ml/ml/model_g ``` As you can see, the `output` field in the first chapter saves the `model_group_id` from the response body. This value is then used in the subsequent chapters to query and delete the model group. -You can also supply defaults for output values, e.g. for `payload._version` used in [cluster/routing/awareness/weights.yaml](tests/routing/cluster/routing/awareness/weights.yaml). +You can combine multiple values, e.g. `task_id: ${task.node}:${task.id}`. + +You can supply defaults for output values, e.g. for `payload._version` used in [cluster/routing/awareness/weights.yaml](tests/routing/cluster/routing/awareness/weights.yaml). ``` version: diff --git a/tests/default/tasks/list.yaml b/tests/default/_core/tasks.yaml similarity index 50% rename from tests/default/tasks/list.yaml rename to tests/default/_core/tasks.yaml index a687e170c..d9ce9a2e1 100644 --- a/tests/default/tasks/list.yaml +++ b/tests/default/_core/tasks.yaml @@ -1,25 +1,35 @@ $schema: ../../../json_schemas/test_story.schema.yaml -description: Test tasks list endpoint. +description: Test tasks endpoint. chapters: - synopsis: List tasks grouped by node. path: /_tasks method: GET parameters: group_by: nodes - response: - status: 200 - synopsis: List tasks grouped by parent. path: /_tasks method: GET parameters: group_by: parents - response: - status: 200 - synopsis: List tasks grouped by none. + id: task path: /_tasks method: GET parameters: group_by: none - response: - status: 200 \ No newline at end of file + output: + id: payload.tasks[0].id + node: payload.tasks[0].node + # - synopsis: Get task by id. + # id: task + # path: /_tasks/{task_id} + # method: GET + # parameters: + # task_id: ${task.node}:${task.id} + # response: + # status: 200 + # payload: + # task: + # node: ${task.node} + # id: ${task.id} diff --git a/tests/default/_core/tasks/cancel.yaml b/tests/default/_core/tasks/cancel.yaml new file mode 100644 index 000000000..fe467270b --- /dev/null +++ b/tests/default/_core/tasks/cancel.yaml @@ -0,0 +1,7 @@ +$schema: ../../../../json_schemas/test_story.schema.yaml + +description: Test tasks endpoint. +chapters: + - synopsis: Cancel all tasks. + path: /_tasks/_cancel + method: POST diff --git a/tools/src/tester/StoryEvaluator.ts b/tools/src/tester/StoryEvaluator.ts index a17df942f..839760dc3 100644 --- a/tools/src/tester/StoryEvaluator.ts +++ b/tools/src/tester/StoryEvaluator.ts @@ -217,10 +217,7 @@ export default class StoryEvaluator { static #extract_params_variables(parameters: Record, variables: Set): void { Object.values(parameters ?? {}).forEach((param) => { if (typeof param === 'string') { - const ref = OutputReference.parse(param) - if (ref) { - variables.add(ref) - } + OutputReference.parse(param).forEach((ref) => variables.add(ref)) } }) } @@ -229,10 +226,7 @@ export default class StoryEvaluator { const request_type = typeof request switch (request_type) { case 'string': { - const ref = OutputReference.parse(request as string) - if (ref !== undefined) { - variables.add(ref) - } + OutputReference.parse(request as string).forEach((ref) => variables.add(ref)) break } case 'object': { diff --git a/tools/src/tester/StoryOutputs.ts b/tools/src/tester/StoryOutputs.ts index 41b8d9e71..838d66763 100644 --- a/tools/src/tester/StoryOutputs.ts +++ b/tools/src/tester/StoryOutputs.ts @@ -48,12 +48,13 @@ export class StoryOutputs { } resolve_string (str: string): any { - const ref = OutputReference.parse(str) - if (ref) { - return this.get_output_value(ref.chapter_id, ref.output_name) - } else { - return str - } + return OutputReference.replace(str, (chapter_id, output_name) => { + if (chapter_id === undefined || output_name === undefined) { + throw new Error(`Invalid output references in ${str}.`) + } + + return this.get_output_value(chapter_id as string, output_name as string) + }) } resolve_value (payload: any): any { diff --git a/tools/src/tester/types/eval.types.ts b/tools/src/tester/types/eval.types.ts index e65b80c3c..f29714afe 100644 --- a/tools/src/tester/types/eval.types.ts +++ b/tools/src/tester/types/eval.types.ts @@ -87,16 +87,42 @@ export enum Result { export class OutputReference { chapter_id: string output_name: string - private constructor (chapter_id: string, output_name: string) { + + private constructor(chapter_id: string, output_name: string) { this.chapter_id = chapter_id this.output_name = output_name } - static parse (str: string): OutputReference | undefined { - if (str.startsWith('${') && str.endsWith('}')) { - const spl = str.slice(2, -1).split('.', 2) - return { chapter_id: spl[0], output_name: spl[1] } + static parse(str: string): OutputReference[] { + const pattern = /\$\{([^}]+)\}/g + let match + var result = [] + while ((match = pattern.exec(str)) !== null) { + const spl = this.#split(match[1], '.', 2) + result.push(new OutputReference(spl[0], spl[1])) } - return undefined + return result + } + + static replace(str: string, callback: (chapter_id: any, variable: any) => string): any { + // preserve type if 1 value is returned + let full_match = str.match(/^\$\{([^}]+)\}$/) + if (full_match) { + const spl = this.#split(full_match[1], '.', 2) + return callback(spl[0], spl[1]) + } else return str.replace(/\$\{([^}]+)\}/g, (_, k) => { + const spl = this.#split(k, '.', 2) + return callback(spl[0], spl[1]) + }); + } + + static #split(str: any, delim: string, count: number): string[] { + if (str === undefined) return [str] + if (count <= 0) return [str] + const parts = str.split(delim) + if (parts.length <= count) return parts + const result = parts.slice(0, count - 1) + result.push(parts.slice(count - 1).join(delim)) + return result } } diff --git a/tools/tests/tester/StoryOutputs.test.ts b/tools/tests/tester/StoryOutputs.test.ts index 3017b7a66..044531328 100644 --- a/tools/tests/tester/StoryOutputs.test.ts +++ b/tools/tests/tester/StoryOutputs.test.ts @@ -55,13 +55,19 @@ test('resolve_params', () => { a: '${chapter_id.x}', b: '${chapter_id.y}', c: 3, - d: 'str' + d: 'str', + e: '${chapter_id.x}:${chapter_id.y}', + f: 'x=${chapter_id.x}', + g: '${chapter_id.y}=y' } expect(story_outputs.resolve_params(parameters)).toEqual({ a: 1, b: 2, c: 3, - d: 'str' + d: 'str', + e: '1:2', + f: 'x=1', + g: '2=y' }) }) diff --git a/tools/tests/types/eval.types.test.ts b/tools/tests/types/eval.types.test.ts new file mode 100644 index 000000000..76f7f3ac2 --- /dev/null +++ b/tools/tests/types/eval.types.test.ts @@ -0,0 +1,23 @@ +/* +* Copyright OpenSearch Contributors +* SPDX-License-Identifier: Apache-2.0 +* +* The OpenSearch Contributors require contributions made to +* this file be licensed under the Apache-2.0 license or a +* compatible open source license. +*/ + +import { OutputReference } from "tester/types/eval.types"; + +describe('OutputReference', () => { + let f = (id: any, k: any): string => `[${id}:${k}]` + + describe('replace', () => { + it('replaces', () => { + expect(OutputReference.replace('string', f)).toEqual('string') + expect(OutputReference.replace('${k.v}', f)).toEqual('[k:v]') + expect(OutputReference.replace('${k.value}', f)).toEqual('[k:value]') + expect(OutputReference.replace('${k.v.m}', f)).toEqual('[k:v.m]') + }) + }) +});