Skip to content

Commit

Permalink
Added detailed test coverage report.
Browse files Browse the repository at this point in the history
Signed-off-by: dblock <[email protected]>
  • Loading branch information
dblock committed Aug 15, 2024
1 parent df34927 commit 695a3f4
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 16 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- Added `read_time`, `write_time`, `queue_size` and `io_time_in_millis` to `IoStatDevice` ([#483](https://github.com/opensearch-project/opensearch-api-specification/pull/483))
- Added `total_rejections_breakup` to `ShardIndexingPressureStats` ([#483](https://github.com/opensearch-project/opensearch-api-specification/pull/483))
- Added `cancelled_task_percentage` and `current_cancellation_eligible_tasks_count` to `ShardSearchBackpressureTaskCancellationStats` ([#483](https://github.com/opensearch-project/opensearch-api-specification/pull/483))
- Added detailed test coverage report ([#513](https://github.com/opensearch-project/opensearch-api-specification/pull/513))

### Changed

Expand Down
32 changes: 32 additions & 0 deletions TESTING_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
- [Warnings](#warnings)
- [multiple-paths-detected](#multiple-paths-detected)
- [Suppressing Warnings](#suppressing-warnings)
- [Collecting Test Coverage](#collecting-test-coverage)
- [Coverage Summary](#coverage-summary)
- [Coverage Report](#coverage-report)
<!-- TOC -->

# Spec Testing Guide
Expand Down Expand Up @@ -318,3 +321,32 @@ The test runner may generate warnings that can be suppressed with `warnings:`. F
parameters:
index: movies
```
## Collecting Test Coverage
### Coverage Summary
The test tool can generate a test coverage summary using `--coverage <path>` with the number of evaluated verb + path combinations, a total number of paths and the % of paths tested.
```json
{
"evaluated_paths_count": 214,
"paths_count": 550,
"evaluated_paths_pct": 38.91
}
```
The report is then used by the [test-spec.yml](.github/workflows/test-spec.yml) workflow, uploaded with every run, combined across various versions of OpenSearch, and reported as a comment on each pull request.
### Coverage Report
The test tool can display detailed and hierarchal test coverage with `--coverage-report`. This is useful to identify untested paths. For example, the [put_alias.yaml](tests/default/indices/aliases/put_alias.yaml) test exercises `PUT /_alias/{name}`, but not the other verbs. The report produces the following output with the missing ones.
```
/_alias (4)
GET /_alias
/{name} (3)
GET /_alias/{name}
POST /_alias/{name}
HEAD /_alias/{name}
```
26 changes: 25 additions & 1 deletion tools/src/tester/ResultLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { type ChapterEvaluation, type Evaluation, Result, type StoryEvaluation }
import { overall_result } from './helpers'
import * as ansi from './Ansi'
import TestResults from './TestResults'
import _ from 'lodash'

export interface ResultLogger {
log: (evaluation: StoryEvaluation) => void
Expand Down Expand Up @@ -43,7 +44,30 @@ export class ConsoleResultLogger implements ResultLogger {

log_coverage(results: TestResults): void {
console.log()
console.log(`Tested ${results.evaluated_paths_count()}/${results.spec_paths_count()} paths.`)
console.log(`Tested ${results.evaluated_paths().length}/${results.spec_paths().length} paths.`)
}

log_coverage_report(results: TestResults): void {
console.log()
console.log(`${results.unevaluated_paths().length} paths remaining.`)
const groups = _.groupBy(results.unevaluated_paths(), (path) => path.split(' ', 2)[1].split('/')[1])
Object.entries(groups).forEach(([root, paths]) => {
this.#log_coverage_group(root, paths)
});
}

#log_coverage_group(key: string, paths: string[], index: number = 2): void {
if (paths.length == 0 || key == undefined) return
console.log(`${' '.repeat(index)}/${key} (${paths.length})`)
const current_level_paths = paths.filter((path) => path.split('/').length == index)
current_level_paths.forEach((path) => {
console.log(`${' '.repeat(index + 2)}${path}`)
})
const next_level_paths = paths.filter((path) => path.split('/').length > index)
const subgroups = _.groupBy(next_level_paths, (path) => path.split('/')[index])
Object.entries(subgroups).forEach(([root, paths]) => {
this.#log_coverage_group(root, paths, index + 1)
});
}

#log_story ({ result, full_path, display_path, message, warnings }: StoryEvaluation): void {
Expand Down
34 changes: 25 additions & 9 deletions tools/src/tester/TestResults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,44 @@ import { write_json } from "../helpers";
export default class TestResults {
protected _spec: MergedOpenApiSpec
protected _evaluations: StoryEvaluations
protected _evaluated_paths?: string[]
protected _unevaluated_paths?: string[]
protected _spec_paths?: string[]

constructor(spec: MergedOpenApiSpec, evaluations: StoryEvaluations) {
this._spec = spec
this._evaluations = evaluations
}

evaluated_paths_count(): number {
return _.uniq(_.compact(_.flatten(_.map(this._evaluations.evaluations, (evaluation) =>
evaluated_paths(): string[] {
if (this._evaluated_paths !== undefined) return this._evaluated_paths
this._evaluated_paths = _.uniq(_.compact(_.flatten(_.map(this._evaluations.evaluations, (evaluation) =>
_.map(evaluation.chapters, (chapter) => chapter.path)
)))).length
))))
return this._evaluated_paths
}

spec_paths_count(): number {
return Object.values(this._spec.paths()).reduce((acc, methods) => acc + methods.length, 0);
unevaluated_paths(): string[] {
if (this._unevaluated_paths !== undefined) return this._unevaluated_paths
this._unevaluated_paths = this.spec_paths().filter((path => !this.evaluated_paths().includes(path)))
return this._unevaluated_paths
}

spec_paths(): string[] {
if (this._spec_paths !== undefined) return this._spec_paths
this._spec_paths = _.uniq(Object.entries(this._spec.paths()).flatMap(([path, path_item]) => {
return Object.values(path_item).map((method) => `${method.toUpperCase()} ${path}`)
}))

return this._spec_paths
}

test_coverage(): SpecTestCoverage {
return {
evaluated_paths_count: this.evaluated_paths_count(),
paths_count: this.spec_paths_count(),
evaluated_paths_pct: this.spec_paths_count() > 0 ? Math.round(
this.evaluated_paths_count() / this.spec_paths_count() * 100 * 100
evaluated_paths_count: this.evaluated_paths().length,
paths_count: this.spec_paths().length,
evaluated_paths_pct: this.spec_paths().length > 0 ? Math.round(
this.evaluated_paths().length / this.spec_paths().length * 100 * 100
) / 100 : 0,
}
}
Expand Down
2 changes: 2 additions & 0 deletions tools/src/tester/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const command = new Command()
.addOption(AWS_REGION_OPTION)
.addOption(AWS_SERVICE_OPTION)
.addOption(new Option('--coverage <path>', 'path to write test coverage results to'))
.addOption(new Option('--coverage-report', 'display a detailed test coverage report'))
.allowExcessArguments(false)
.parse()

Expand All @@ -82,6 +83,7 @@ runner.run(opts.testsPath, spec.api_version(), opts.opensearchDistribution, opts

const test_results = new TestResults(spec, results)
result_logger.log_coverage(test_results)
if (opts.coverageReport) result_logger.log_coverage_report(test_results)
if (opts.coverage !== undefined) {
console.log(`Writing ${opts.coverage} ...`)
test_results.write_coverage(opts.coverage)
Expand Down
35 changes: 35 additions & 0 deletions tools/tests/tester/ResultLogger.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,41 @@ describe('ConsoleResultLogger', () => {
])
})

test('log_coverage_report', () => {
const spec = new MergedOpenApiSpec('tools/tests/tester/fixtures/specs/complete')
const test_results = new TestResults(spec, { evaluations: [{
result: Result.PASSED,
display_path: 'path',
full_path: 'path',
description: 'description',
chapters: [
{
title: 'title',
overall: { result: Result.PASSED },
path: 'GET /_nodes/{id}'
}
]
}] })

logger.log_coverage_report(test_results)

expect(log.mock.calls).not.toContain(["GET /_nodes/{id}"])
expect(log.mock.calls).toEqual([
[],
["5 paths remaining."],
[" /_nodes (1)"],
[" /{id} (1)"],
[" POST /_nodes/{id}"],
[" /cluster_manager (2)"],
[" GET /cluster_manager"],
[" POST /cluster_manager"],
[" /index (1)"],
[" GET /index"],
[" /nodes (1)"],
[" GET /nodes"]
])
})

test('with retries', () => {
logger.log({
result: Result.PASSED,
Expand Down
30 changes: 24 additions & 6 deletions tools/tests/tester/TestResults.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('TestResults', () => {

const evaluations = [{
result: Result.PASSED,
display_path: 'path',
display_path: 'PUT /{index}',
full_path: 'full_path',
description: 'description',
message: 'message',
Expand All @@ -26,20 +26,38 @@ describe('TestResults', () => {
overall: {
result: Result.PASSED
},
path: 'path'
path: 'PUT /{index}'
}],
epilogues: [],
prologues: []
}]

const test_results = new TestResults(spec, { evaluations })

test('evaluated_paths_count', () => {
expect(test_results.evaluated_paths_count()).toEqual(1)
test('unevaluated_paths', () => {
expect(test_results.unevaluated_paths()).toEqual([
"GET /_nodes/{id}",
"POST /_nodes/{id}",
"GET /cluster_manager",
"POST /cluster_manager",
"GET /index",
"GET /nodes"
])
})

test('spec_paths_count', () => {
expect(test_results.spec_paths_count()).toEqual(6)
test('evaluated_paths', () => {
expect(test_results.evaluated_paths()).toEqual(['PUT /{index}'])
})

test('spec_paths', () => {
expect(test_results.spec_paths()).toEqual([
"GET /_nodes/{id}",
"POST /_nodes/{id}",
"GET /cluster_manager",
"POST /cluster_manager",
"GET /index",
"GET /nodes"
])
})

test('write_coverage', () => {
Expand Down

0 comments on commit 695a3f4

Please sign in to comment.