Commit 425380d8 authored by Vitali Tatarintev's avatar Vitali Tatarintev

Merge branch 'be-test-suite-graphql' into 'master'

Add test suite endpoint to GraphQL

See merge request gitlab-org/gitlab!58924
parents 44572f31 5c7c2601
# frozen_string_literal: true
module Resolvers
module Ci
class TestSuiteResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource
type ::Types::Ci::TestSuiteType, null: true
authorize :read_build
authorizes_object!
alias_method :pipeline, :object
argument :build_ids, [GraphQL::ID_TYPE],
required: true,
description: 'IDs of the builds used to run the test suite.'
def resolve(build_ids:)
builds = pipeline.latest_builds.id_in(build_ids).presence
return unless builds
TestSuiteSerializer
.new(project: pipeline.project, current_user: @current_user)
.represent(load_test_suite_data(builds), details: true)
end
private
def load_test_suite_data(builds)
suite = builds.sum do |build|
build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new)
end
Gitlab::Ci::Reports::TestFailureHistory.new(suite.failed.values, pipeline.project).load!
suite
end
end
end
end
......@@ -128,6 +128,12 @@ module Types
description: 'Summary of the test report generated by the pipeline.',
resolver: Resolvers::Ci::TestReportSummaryResolver
field :test_suite,
Types::Ci::TestSuiteType,
null: true,
description: 'A specific test suite in a pipeline test report.',
resolver: Resolvers::Ci::TestSuiteResolver
def detailed_status
object.detailed_status(current_user)
end
......
# frozen_string_literal: true
module Types
module Ci
# rubocop: disable Graphql/AuthorizeTypes
class RecentFailuresType < BaseObject
graphql_name 'RecentFailures'
description 'Recent failure history of a test case.'
connection_type_class(Types::CountableConnectionType)
field :count, GraphQL::INT_TYPE, null: true,
description: 'Number of times the test case has failed in the past 14 days.'
field :base_branch, GraphQL::STRING_TYPE, null: true,
description: 'Name of the base branch of the project.'
end
# rubocop: enable Graphql/AuthorizeTypes
end
end
# frozen_string_literal: true
module Types
module Ci
class TestCaseStatusEnum < BaseEnum
graphql_name 'TestCaseStatus'
::Gitlab::Ci::Reports::TestCase::STATUS_TYPES.each do |status|
value status,
description: "Test case that has a status of #{status}.",
value: status
end
end
end
end
# frozen_string_literal: true
module Types
module Ci
# rubocop: disable Graphql/AuthorizeTypes
class TestCaseType < BaseObject
graphql_name 'TestCase'
description 'Test case in pipeline test report.'
connection_type_class(Types::CountableConnectionType)
field :status, Types::Ci::TestCaseStatusEnum, null: true,
description: "Status of the test case (#{::Gitlab::Ci::Reports::TestCase::STATUS_TYPES.join(', ')})."
field :name, GraphQL::STRING_TYPE, null: true,
description: 'Name of the test case.'
field :classname, GraphQL::STRING_TYPE, null: true,
description: 'Classname of the test case.'
field :execution_time, GraphQL::FLOAT_TYPE, null: true,
description: 'Test case execution time in seconds.'
field :file, GraphQL::STRING_TYPE, null: true,
description: 'Path to the file of the test case.'
field :attachment_url, GraphQL::STRING_TYPE, null: true,
description: 'URL of the test case attachment file.'
field :system_output, GraphQL::STRING_TYPE, null: true,
description: 'System output of the test case.'
field :stack_trace, GraphQL::STRING_TYPE, null: true,
description: 'Stack trace of the test case.'
field :recent_failures, Types::Ci::RecentFailuresType, null: true,
description: 'Recent failure history of the test case on the base branch.'
end
# rubocop: enable Graphql/AuthorizeTypes
end
end
# frozen_string_literal: true
module Types
module Ci
# rubocop: disable Graphql/AuthorizeTypes
class TestSuiteType < BaseObject
graphql_name 'TestSuite'
description 'Test suite in a pipeline test report.'
connection_type_class(Types::CountableConnectionType)
field :name, GraphQL::STRING_TYPE, null: true,
description: 'Name of the test suite.'
field :total_time, GraphQL::FLOAT_TYPE, null: true,
description: 'Total duration of the tests in the test suite.'
field :total_count, GraphQL::INT_TYPE, null: true,
description: 'Total number of the test cases in the test suite.'
field :success_count, GraphQL::INT_TYPE, null: true,
description: 'Total number of test cases that succeeded in the test suite.'
field :failed_count, GraphQL::INT_TYPE, null: true,
description: 'Total number of test cases that failed in the test suite.'
field :skipped_count, GraphQL::INT_TYPE, null: true,
description: 'Total number of test cases that were skipped in the test suite.'
field :error_count, GraphQL::INT_TYPE, null: true,
description: 'Total number of test cases that had an error.'
field :suite_error, GraphQL::STRING_TYPE, null: true,
description: 'Test suite error message.'
field :test_cases, Types::Ci::TestCaseType.connection_type, null: true,
description: 'Test cases in the test suite.'
end
# rubocop: enable Graphql/AuthorizeTypes
end
end
---
title: Add GraphQL endpoint for a specific test suite in pipelines
merge_request: 58924
author:
type: added
......@@ -4827,6 +4827,7 @@ Information about pagination in a connection.
| `startedAt` | [`Time`](#time) | Timestamp when the pipeline was started. |
| `status` | [`PipelineStatusEnum!`](#pipelinestatusenum) | Status of the pipeline (CREATED, WAITING_FOR_RESOURCE, PREPARING, PENDING, RUNNING, FAILED, SUCCESS, CANCELED, SKIPPED, MANUAL, SCHEDULED). |
| `testReportSummary` | [`TestReportSummary!`](#testreportsummary) | Summary of the test report generated by the pipeline. |
| `testSuite` | [`TestSuite`](#testsuite) | A specific test suite in a pipeline test report. |
| `updatedAt` | [`Time!`](#time) | Timestamp of the pipeline's last activity. |
| `upstream` | [`Pipeline`](#pipeline) | Pipeline that triggered the pipeline. |
| `user` | [`User`](#user) | Pipeline user. |
......@@ -5276,6 +5277,15 @@ Represents rules that commit pushes must follow.
| ----- | ---- | ----------- |
| `rejectUnsignedCommits` | [`Boolean!`](#boolean) | Indicates whether commits not signed through GPG will be rejected. |
### `RecentFailures`
Recent failure history of a test case.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `baseBranch` | [`String`](#string) | Name of the base branch of the project. |
| `count` | [`Int`](#int) | Number of times the test case has failed in the past 14 days. |
### `Release`
Represents a release.
......@@ -6364,6 +6374,42 @@ An edge in a connection.
| `cursor` | [`String!`](#string) | A cursor for use in pagination. |
| `node` | [`TerraformStateVersionRegistry`](#terraformstateversionregistry) | The item at the end of the edge. |
### `TestCase`
Test case in pipeline test report.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `attachmentUrl` | [`String`](#string) | URL of the test case attachment file. |
| `classname` | [`String`](#string) | Classname of the test case. |
| `executionTime` | [`Float`](#float) | Test case execution time in seconds. |
| `file` | [`String`](#string) | Path to the file of the test case. |
| `name` | [`String`](#string) | Name of the test case. |
| `recentFailures` | [`RecentFailures`](#recentfailures) | Recent failure history of the test case on the base branch. |
| `stackTrace` | [`String`](#string) | Stack trace of the test case. |
| `status` | [`TestCaseStatus`](#testcasestatus) | Status of the test case (error, failed, success, skipped). |
| `systemOutput` | [`String`](#string) | System output of the test case. |
### `TestCaseConnection`
The connection type for TestCase.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `count` | [`Int!`](#int) | Total count of collection. |
| `edges` | [`[TestCaseEdge]`](#testcaseedge) | A list of edges. |
| `nodes` | [`[TestCase]`](#testcase) | A list of nodes. |
| `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. |
### `TestCaseEdge`
An edge in a connection.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `cursor` | [`String!`](#string) | A cursor for use in pagination. |
| `node` | [`TestCase`](#testcase) | The item at the end of the edge. |
### `TestReport`
Represents a requirement test report.
......@@ -6417,6 +6463,22 @@ Total test report statistics.
| `suiteError` | [`String`](#string) | Test suite error message. |
| `time` | [`Float`](#float) | Total duration of the tests. |
### `TestSuite`
Test suite in a pipeline test report.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `errorCount` | [`Int`](#int) | Total number of test cases that had an error. |
| `failedCount` | [`Int`](#int) | Total number of test cases that failed in the test suite. |
| `name` | [`String`](#string) | Name of the test suite. |
| `skippedCount` | [`Int`](#int) | Total number of test cases that were skipped in the test suite. |
| `successCount` | [`Int`](#int) | Total number of test cases that succeeded in the test suite. |
| `suiteError` | [`String`](#string) | Test suite error message. |
| `testCases` | [`TestCaseConnection`](#testcaseconnection) | Test cases in the test suite. |
| `totalCount` | [`Int`](#int) | Total number of the test cases in the test suite. |
| `totalTime` | [`Float`](#float) | Total duration of the tests in the test suite. |
### `TestSuiteSummary`
Test suite summary in a pipeline test report.
......@@ -8455,6 +8517,15 @@ Common sort values.
| `updated_asc` **{warning-solid}** | **Deprecated:** This was renamed. Please use `UPDATED_ASC`. Deprecated in 13.5. |
| `updated_desc` **{warning-solid}** | **Deprecated:** This was renamed. Please use `UPDATED_DESC`. Deprecated in 13.5. |
### `TestCaseStatus`
| Value | Description |
| ----- | ----------- |
| `error` | Test case that has a status of error. |
| `failed` | Test case that has a status of failed. |
| `skipped` | Test case that has a status of skipped. |
| `success` | Test case that has a status of success. |
### `TestReportState`
State of a test report.
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::Ci::TestSuiteResolver do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project, :public, :repository) }
describe '#resolve' do
subject(:test_suite) { resolve(described_class, obj: pipeline, args: { build_ids: build_ids }) }
context 'when pipeline has builds with test reports' do
let_it_be(:main_pipeline) { create(:ci_pipeline, :with_test_reports_with_three_failures, project: project) }
let_it_be(:pipeline) { create(:ci_pipeline, :with_test_reports_with_three_failures, project: project, ref: 'new-feature') }
let(:suite_name) { 'test' }
let(:build_ids) { pipeline.latest_builds.pluck(:id) }
before do
build = main_pipeline.builds.last
build.update_column(:finished_at, 1.day.ago) # Just to be sure we are included in the report window
# The JUnit fixture for the given build has 3 failures.
# This service will create 1 test case failure record for each.
Ci::TestFailureHistoryService.new(main_pipeline).execute
end
it 'renders test suite data' do
expect(test_suite[:name]).to eq('test')
# Each test failure in this pipeline has a matching failure in the default branch
recent_failures = test_suite[:test_cases].map { |tc| tc[:recent_failures] }
expect(recent_failures).to eq([
{ count: 1, base_branch: 'master' },
{ count: 1, base_branch: 'master' },
{ count: 1, base_branch: 'master' }
])
end
end
context 'when pipeline has no builds that matches the given build_ids' do
let_it_be(:pipeline) { create(:ci_empty_pipeline) }
let(:suite_name) { 'test' }
let(:build_ids) { [non_existing_record_id] }
it 'returns nil' do
expect(test_suite).to be_nil
end
end
end
end
......@@ -13,7 +13,7 @@ RSpec.describe Types::Ci::PipelineType do
coverage created_at updated_at started_at finished_at committed_at
stages user retryable cancelable jobs source_job job downstream
upstream path project active user_permissions warnings commit_path uses_needs
test_report_summary
test_report_summary test_suite
]
if Gitlab.ee?
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::Ci::RecentFailuresType do
specify { expect(described_class.graphql_name).to eq('RecentFailures') }
it 'contains attributes related to a recent failure history for a test case' do
expected_fields = %w[
count base_branch
]
expect(described_class).to have_graphql_fields(*expected_fields)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::Ci::TestCaseStatusEnum do
specify { expect(described_class.graphql_name).to eq('TestCaseStatus') }
it 'exposes all test case status types' do
expect(described_class.values.keys).to eq(
::Gitlab::Ci::Reports::TestCase::STATUS_TYPES
)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::Ci::TestCaseType do
specify { expect(described_class.graphql_name).to eq('TestCase') }
it 'contains attributes related to a pipeline test case' do
expected_fields = %w[
name status classname file attachment_url execution_time stack_trace system_output recent_failures
]
expect(described_class).to have_graphql_fields(*expected_fields)
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Types::Ci::TestSuiteType do
specify { expect(described_class.graphql_name).to eq('TestSuite') }
it 'contains attributes related to a pipeline test suite' do
expected_fields = %w[
name total_time total_count success_count failed_count skipped_count error_count suite_error test_cases
]
expect(described_class).to have_graphql_fields(*expected_fields)
end
end
......@@ -235,4 +235,51 @@ RSpec.describe 'getting pipeline information nested in a project' do
end
end
end
context 'when requesting a specific test suite' do
let_it_be(:pipeline) { create(:ci_pipeline, :with_test_reports, project: project) }
let(:suite_name) { 'test' }
let_it_be(:build_ids) { pipeline.latest_builds.pluck(:id) }
let(:variables) do
{
path: project.full_path,
pipelineIID: pipeline.iid.to_s
}
end
let(:query) do
<<~GQL
query($path: ID!, $pipelineIID: ID!, $buildIds: [ID!]!) {
project(fullPath: $path) {
pipeline(iid: $pipelineIID) {
testSuite(buildIds: $buildIds) {
name
}
}
}
}
GQL
end
it 'can request a test suite by an array of build_ids' do
vars = variables.merge(buildIds: build_ids)
post_graphql(query, current_user: current_user, variables: vars)
expect(graphql_data_at(:project, :pipeline, :testSuite, :name)).to eq(suite_name)
end
context 'when pipeline has no builds that matches the given build_ids' do
let_it_be(:build_ids) { [non_existing_record_id] }
it 'returns nil' do
vars = variables.merge(buildIds: build_ids)
post_graphql(query, current_user: current_user, variables: vars)
expect(graphql_data_at(*path, :test_suite)).to be_nil
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment