Commit bd3df45f authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 25ab40a4 7018506a
#import "./release_for_editing.fragment.graphql"
query oneReleaseForEditing($fullPath: ID!, $tagName: String!) {
project(fullPath: $fullPath) {
release(tagName: $tagName) {
...ReleaseForEditing
}
}
}
fragment ReleaseForEditing on Release {
name
tagName
description
assets {
links {
nodes {
id
name
url
linkType
}
}
}
links {
selfUrl
}
milestones {
nodes {
title
}
}
}
......@@ -52,24 +52,37 @@ const convertScalarProperties = (graphQLRelease) =>
'name',
'tagName',
'tagPath',
'description',
'descriptionHtml',
'releasedAt',
'upcomingRelease',
]);
const convertAssets = (graphQLRelease) => ({
assets: {
count: graphQLRelease.assets.count,
sources: [...graphQLRelease.assets.sources.nodes],
links: graphQLRelease.assets.links.nodes.map((l) => ({
const convertAssets = (graphQLRelease) => {
let sources = [];
if (graphQLRelease.assets.sources?.nodes) {
sources = [...graphQLRelease.assets.sources.nodes];
}
let links = [];
if (graphQLRelease.assets.links?.nodes) {
links = graphQLRelease.assets.links.nodes.map((l) => ({
...l,
linkType: l.linkType?.toLowerCase(),
})),
},
});
}));
}
return {
assets: {
count: graphQLRelease.assets.count,
sources,
links,
},
};
};
const convertEvidences = (graphQLRelease) => ({
evidences: graphQLRelease.evidences.nodes.map((e) => e),
evidences: (graphQLRelease.evidences?.nodes ?? []).map((e) => ({ ...e })),
});
const convertLinks = (graphQLRelease) => ({
......@@ -100,10 +113,12 @@ const convertMilestones = (graphQLRelease) => ({
...m,
webUrl: m.webPath,
webPath: undefined,
issueStats: {
total: m.stats.totalIssuesCount,
closed: m.stats.closedIssuesCount,
},
issueStats: m.stats
? {
total: m.stats.totalIssuesCount,
closed: m.stats.closedIssuesCount,
}
: {},
stats: undefined,
})),
});
......
......@@ -23,7 +23,7 @@ module Types
field :stage, Types::Ci::StageType, null: true,
description: 'Stage of the job.'
field :allow_failure, ::GraphQL::BOOLEAN_TYPE, null: false,
description: 'Whether this job is allowed to fail.'
description: 'Whether the job is allowed to fail.'
field :duration, GraphQL::INT_TYPE, null: true,
description: 'Duration of the job in seconds.'
field :tags, [GraphQL::STRING_TYPE], null: true,
......@@ -41,6 +41,12 @@ module Types
field :scheduled_at, Types::TimeType, null: true,
description: 'Schedule for the build.'
# Life-cycle durations:
field :queued_duration,
type: Types::DurationType,
null: true,
description: 'How long the job was enqueued before starting.'
field :detailed_status, Types::Ci::DetailedStatusType, null: true,
description: 'Detailed status of the job.'
field :artifacts, Types::Ci::JobArtifactType.connection_type, null: true,
......
......@@ -39,6 +39,9 @@ module Types
field :duration, GraphQL::INT_TYPE, null: true,
description: 'Duration of the pipeline in seconds.'
field :queued_duration, Types::DurationType, null: true,
description: 'How long the pipeline was queued before starting.'
field :coverage, GraphQL::FLOAT_TYPE, null: true,
description: 'Coverage percentage.'
......
# frozen_string_literal: true
module Types
class DurationType < BaseScalar
graphql_name 'Duration'
description <<~DESC
Duration between two instants, represented as a fractional number of seconds.
For example: 12.3334
DESC
def self.coerce_input(value, ctx)
case value
when Float
value
when Integer
value.to_f
when NilClass
raise GraphQL::CoercionError, 'Cannot be nil'
else
raise GraphQL::CoercionError, "Expected number: got #{value.class}"
end
end
def self.coerce_result(value, ctx)
value.to_f
end
end
end
......@@ -1047,7 +1047,7 @@ module Ci
end
def build_data
@build_data ||= Gitlab::DataBuilder::Build.build(self)
strong_memoize(:build_data) { Gitlab::DataBuilder::Build.build(self) }
end
def successful_deployment_status
......
......@@ -214,8 +214,14 @@ class CommitStatus < ApplicationRecord
allow_failure? && (failed? || canceled?)
end
# Time spent running.
def duration
calculate_duration
calculate_duration(started_at, finished_at)
end
# Time spent in the pending state.
def queued_duration
calculate_duration(queued_at, started_at)
end
def latest?
......
......@@ -122,12 +122,10 @@ module Ci
private
def calculate_duration
if started_at && finished_at
finished_at - started_at
elsif started_at
Time.current - started_at
end
def calculate_duration(start_time, end_time)
return unless start_time
(end_time || Time.current) - start_time
end
end
end
---
title: Expose job and project queued duration in all APIs
merge_request: 59901
author:
type: changed
......@@ -15,7 +15,7 @@ performance, data, or could even exhaust the allocated resources for the applica
Rate limits can be used to improve the security and durability of GitLab.
For example, a simple script can make thousands of web requests per second. Whether malicious, apathetic, or just a bug, your application and infrastructure may not be able to cope with the load. Rate limits can help mitigate these types of attacks.
For example, one script can make thousands of web requests per second. Whether malicious, apathetic, or just a bug, your application and infrastructure may not be able to cope with the load. Rate limits can help to mitigate these types of attacks.
Read more about [configuring rate limits](../security/rate_limits.md) in the Security documentation.
......@@ -25,17 +25,17 @@ Read more about [configuring rate limits](../security/rate_limits.md) in the Sec
This setting limits the request rate to the issue creation endpoint.
Read more on [issue creation rate limits](../user/admin_area/settings/rate_limit_on_issues_creation.md).
Read more about [issue creation rate limits](../user/admin_area/settings/rate_limit_on_issues_creation.md).
- **Default rate limit** - Disabled by default
- **Default rate limit**: Disabled by default.
### By User or IP
This setting limits the request rate per user or IP.
Read more on [User and IP rate limits](../user/admin_area/settings/user_and_ip_rate_limits.md).
Read more about [User and IP rate limits](../user/admin_area/settings/user_and_ip_rate_limits.md).
- **Default rate limit** - Disabled by default
- **Default rate limit**: Disabled by default.
### By raw endpoint
......@@ -43,9 +43,9 @@ Read more on [User and IP rate limits](../user/admin_area/settings/user_and_ip_r
This setting limits the request rate per endpoint.
Read more on [raw endpoint rate limits](../user/admin_area/settings/rate_limits_on_raw_endpoints.md).
Read more about [raw endpoint rate limits](../user/admin_area/settings/rate_limits_on_raw_endpoints.md).
- **Default rate limit** - 300 requests per project, per commit and per file path
- **Default rate limit**: 300 requests per project, per commit and per file path.
### By protected path
......@@ -65,9 +65,9 @@ GitLab rate limits the following paths by default:
'/admin/session'
```
Read more on [protected path rate limits](../user/admin_area/settings/protected_paths.md).
Read more about [protected path rate limits](../user/admin_area/settings/protected_paths.md).
- **Default rate limit** - After 10 requests, the client must wait 60 seconds before trying again
- **Default rate limit**: After 10 requests, the client must wait 60 seconds before trying again.
### Package Registry
......@@ -76,7 +76,7 @@ Read more on [protected path rate limits](../user/admin_area/settings/protected_
This setting limits the request rate on the Packages API per user or IP. For more information, see
[Package Registry Rate Limits](../user/admin_area/settings/package_registry_rate_limits.md).
- **Default rate limit:** Disabled by default
- **Default rate limit**: Disabled by default.
### Import/Export
......@@ -84,16 +84,16 @@ This setting limits the request rate on the Packages API per user or IP. For mor
This setting limits the import/export actions for groups and projects.
| Limit | Default (per minute per user) |
| ----- | ----------------------------- |
| Project Import | 6 |
| Project Export | 6 |
| Limit | Default (per minute per user) |
|-------------------------|-------------------------------|
| Project Import | 6 |
| Project Export | 6 |
| Project Export Download | 1 |
| Group Import | 6 |
| Group Export | 6 |
| Group Export | Download | 1 |
| Group Import | 6 |
| Group Export | 6 |
| Group Export Download | 1 |
Read more on [import/export rate limits](../user/admin_area/settings/import_export_rate_limits.md).
Read more about [import/export rate limits](../user/admin_area/settings/import_export_rate_limits.md).
### Rack attack
......@@ -101,9 +101,9 @@ This method of rate limiting is cumbersome, but has some advantages. It allows
throttling of specific paths, and is also integrated into Git and container
registry requests.
Read more on the [Rack Attack initializer](../security/rack_attack.md) method of setting rate limits.
Read more about the [Rack Attack initializer](../security/rack_attack.md) method of setting rate limits.
- **Default rate limit** - Disabled
- **Default rate limit**: Disabled.
### Member Invitations
......@@ -116,11 +116,11 @@ Limit the maximum daily member invitations allowed per group hierarchy.
Clone traffic can put a large strain on your Gitaly service. To prevent such workloads from overwhelming your Gitaly server, you can set concurrency limits in Gitaly's configuration file.
Read more on [Gitaly concurrency limits](gitaly/configure_gitaly.md#limit-rpc-concurrency).
Read more about [Gitaly concurrency limits](gitaly/configure_gitaly.md#limit-rpc-concurrency).
- **Default rate limit** - Disabled
- **Default rate limit**: Disabled.
## Number of comments per issue, merge request or commit
## Number of comments per issue, merge request, or commit
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/22388) in GitLab 12.4.
......@@ -129,7 +129,7 @@ merge request, or commit. When the limit is reached, system notes can still be
added so that the history of events is not lost, but user-submitted comments
will fail.
- **Max limit:** 5.000 comments
- **Max limit**: 5,000 comments.
## Size of comments and descriptions of issues, merge requests, and epics
......@@ -141,7 +141,7 @@ item will not be created.
It's possible that this limit will be changed to a lower number in the future.
- **Max size:** ~1 million characters / ~1 MB
- **Max size**: ~1 million characters / ~1 MB.
## Size of commit titles and descriptions
......@@ -161,7 +161,7 @@ The maximum number of issues loaded on the milestone overview page is 500.
When the number exceeds the limit the page displays an alert and links to a paginated
[issue list](../user/project/issues/managing_issues.md) of all issues in the milestone.
- **Limit:** 500 issues
- **Limit**: 500 issues.
## Number of pipelines per Git push
......@@ -183,13 +183,13 @@ Activity history for projects and individuals' profiles was limited to one year
There is a limit when embedding metrics in GFM for performance reasons.
- **Max limit:** 100 embeds
- **Max limit**: 100 embeds.
## Number of webhooks
On GitLab.com, the [maximum number of webhooks and their size](../user/gitlab_com/index.md#webhooks) per project, and per group, is limited.
To set this limit on a self-managed installation, where the default is `100` project webhooks and `50` group webhooks, run the following in the
To set this limit for a self-managed installation, where the default is `100` project webhooks and `50` group webhooks, run the following in the
[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session):
```ruby
......@@ -212,7 +212,7 @@ Set the limit to `0` to disable it.
The [minimum time between pull refreshes](../user/project/repository/repository_mirroring.md)
defaults to 300 seconds (5 minutes).
To change this limit on a self-managed installation, run the following in the
To change this limit for a self-managed installation, run the following in the
[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session):
```ruby
......@@ -229,14 +229,14 @@ Plan.default.actual_limits.update!(pull_mirror_interval_seconds: 200)
GitLab ignores all incoming emails sent from auto-responders by looking for the `X-Autoreply`
header. Such emails don't create comments on issues or merge requests.
## Amount of data sent from Sentry via Error Tracking
## Amount of data sent from Sentry through Error Tracking
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14926) in GitLab 12.6.
Sentry payloads sent to GitLab have a 1 MB maximum limit, both for security reasons
and to limit memory consumption.
## Max offset allowed via REST API for offset-based pagination
## Max offset allowed by the REST API for offset-based pagination
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/34565) in GitLab 13.0.
......@@ -245,7 +245,7 @@ requested offset into the set of results. This limit is only applied to endpoint
support keyset-based pagination. More information about pagination options can be
found in the [API docs section on pagination](../api/README.md#pagination).
To set this limit on a self-managed installation, run the following in the
To set this limit for a self-managed installation, run the following in the
[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session):
```ruby
......@@ -255,7 +255,7 @@ To set this limit on a self-managed installation, run the following in the
Plan.default.actual_limits.update!(offset_pagination_limit: 10000)
```
- **Default offset pagination limit:** 50000
- **Default offset pagination limit**: `50000`.
Set the limit to `0` to disable it.
......@@ -281,7 +281,7 @@ will fail with a `job_activity_limit_exceeded` error.
higher installations, this limit is defined under a `default` plan that affects all
projects. This limit is disabled (`0`) by default.
To set this limit on a self-managed installation, run the following in the
To set this limit for a self-managed installation, run the following in the
[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session):
```ruby
......@@ -304,7 +304,7 @@ too many deployments fail with a `deployments_limit_exceeded` error.
The default limit is 500 for all [GitLab self-managed and SaaS plans](https://about.gitlab.com/pricing/).
To change the limit on a self-managed installation, change the `default` plan's limit with the following
To change the limit for a self-managed installation, change the `default` plan's limit with the following
[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session) command:
```ruby
......@@ -332,7 +332,7 @@ limit, the subscription will be considered invalid.
or higher installations, this limit is defined under a `default` plan that
affects all projects. By default, there is a limit of `2` subscriptions.
To set this limit on a self-managed installation, run the following in the
To set this limit for a self-managed installation, run the following in the
[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session):
```ruby
......@@ -357,7 +357,7 @@ On [GitLab Premium](https://about.gitlab.com/pricing/) self-managed or
higher installations, this limit is defined under a `default` plan that affects all
projects. By default, there is a limit of `10` pipeline schedules.
To set this limit on a self-managed installation, run the following in the
To set this limit for a self-managed installation, run the following in the
[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session):
```ruby
......@@ -505,11 +505,11 @@ See [Environment Dashboard](../ci/environments/environments_dashboard.md#adding-
Pods and Deployments. However, data over 10 MB for a certain environment read from
Kubernetes won't be shown.
## Merge Request reports
## Merge request reports
Reports that go over the 20 MB limit won't be loaded. Affected reports:
- [Merge Request security reports](../user/project/merge_requests/testing_and_reports_in_merge_requests.md#security-reports)
- [Merge request security reports](../user/project/merge_requests/testing_and_reports_in_merge_requests.md#security-reports)
- [CI/CD parameter `artifacts:expose_as`](../ci/yaml/README.md#artifactsexpose_as)
- [Unit test reports](../ci/unit_test_reports.md)
......@@ -523,8 +523,8 @@ You can set a limit on the content of repository files that are indexed in
Elasticsearch. Any files larger than this limit will not be indexed, and thus
will not be searchable.
Setting a limit helps reduce the memory usage of the indexing processes as well
as the overall index size. This value defaults to `1024 KiB` (1 MiB) as any
Setting a limit helps reduce the memory usage of the indexing processes and
the overall index size. This value defaults to `1024 KiB` (1 MiB) as any
text files larger than this likely aren't meant to be read by humans.
You must set a limit, as unlimited file sizes aren't supported. Setting this
......@@ -544,8 +544,8 @@ This is applicable to all indexed data except repository files that get
indexed, which have a separate limit (see [Maximum file size
indexed](#maximum-file-size-indexed)).
- On GitLab.com this is limited to 20000 characters
- For self-managed installations it is unlimited by default
- On GitLab.com, this is limited to 20,000 characters
- For self-managed installations, this is unlimited by default
This limit can be configured for self-managed installations when [enabling
Elasticsearch](../integration/elasticsearch.md#enabling-advanced-search).
......@@ -559,7 +559,7 @@ Set the limit to `0` to disable it.
## Snippets limits
See the [documentation on Snippets settings](snippets/index.md).
See the [documentation about Snippets settings](snippets/index.md).
## Design Management limits
......@@ -596,14 +596,14 @@ More information can be found in the [Push event activities limit and bulk push
On GitLab.com, the maximum file size for a package that's uploaded to the [GitLab Package Registry](../user/packages/package_registry/index.md) varies by format:
- Conan: 5GB
- Generic: 5GB
- Maven: 5GB
- npm: 5GB
- NuGet: 5GB
- PyPI: 5GB
- Conan: 5 GB
- Generic: 5 GB
- Maven: 5 GB
- npm: 5 GB
- NuGet: 5 GB
- PyPI: 5 GB
To set this limit on a self-managed installation, run the following in the
To set this limit for a self-managed installation, run the following in the
[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session):
```ruby
......
......@@ -7162,7 +7162,7 @@ Represents the total number of issues and their weights for a particular day.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="cijobactive"></a>`active` | [`Boolean!`](#boolean) | Indicates the job is active. |
| <a id="cijoballowfailure"></a>`allowFailure` | [`Boolean!`](#boolean) | Whether this job is allowed to fail. |
| <a id="cijoballowfailure"></a>`allowFailure` | [`Boolean!`](#boolean) | Whether the job is allowed to fail. |
| <a id="cijobartifacts"></a>`artifacts` | [`CiJobArtifactConnection`](#cijobartifactconnection) | Artifacts generated by the job. |
| <a id="cijobcancelable"></a>`cancelable` | [`Boolean!`](#boolean) | Indicates the job can be canceled. |
| <a id="cijobcommitpath"></a>`commitPath` | [`String`](#string) | Path to the commit that triggered the job. |
......@@ -7179,6 +7179,7 @@ Represents the total number of issues and their weights for a particular day.
| <a id="cijobpipeline"></a>`pipeline` | [`Pipeline`](#pipeline) | Pipeline the job belongs to. |
| <a id="cijobplayable"></a>`playable` | [`Boolean!`](#boolean) | Indicates the job can be played. |
| <a id="cijobqueuedat"></a>`queuedAt` | [`Time`](#time) | When the job was enqueued and marked as pending. |
| <a id="cijobqueuedduration"></a>`queuedDuration` | [`Duration`](#duration) | How long the job was enqueued before starting. |
| <a id="cijobrefname"></a>`refName` | [`String`](#string) | Ref name of the job. |
| <a id="cijobrefpath"></a>`refPath` | [`String`](#string) | Path to the ref. |
| <a id="cijobretryable"></a>`retryable` | [`Boolean!`](#boolean) | Indicates the job can be retried. |
......@@ -10354,6 +10355,7 @@ Information about pagination in a connection.
| <a id="pipelineiid"></a>`iid` | [`String!`](#string) | Internal ID of the pipeline. |
| <a id="pipelinepath"></a>`path` | [`String`](#string) | Relative path to the pipeline's page. |
| <a id="pipelineproject"></a>`project` | [`Project`](#project) | Project the pipeline belongs to. |
| <a id="pipelinequeuedduration"></a>`queuedDuration` | [`Duration`](#duration) | How long the pipeline was queued before starting. |
| <a id="pipelineretryable"></a>`retryable` | [`Boolean!`](#boolean) | Specifies if a pipeline can be retried. |
| <a id="pipelinesecurityreportsummary"></a>`securityReportSummary` | [`SecurityReportSummary`](#securityreportsummary) | Vulnerability and scanned resource counts for each security scanner of the pipeline. |
| <a id="pipelinesha"></a>`sha` | [`String!`](#string) | SHA of the pipeline's commit. |
......@@ -14464,6 +14466,12 @@ A `DiscussionID` is a global ID. It is encoded as a string.
An example `DiscussionID` is: `"gid://gitlab/Discussion/1"`.
### `Duration`
Duration between two instants, represented as a fractional number of seconds.
For example: 12.3334.
### `EnvironmentID`
A `EnvironmentID` is a global ID. It is encoded as a string.
......
......@@ -43,6 +43,7 @@ Example of response
"started_at": "2015-12-24T17:54:27.722Z",
"finished_at": "2015-12-24T17:54:27.895Z",
"duration": 0.173,
"queued_duration": 0.010,
"artifacts_file": {
"filename": "artifacts.zip",
"size": 1000
......@@ -107,6 +108,7 @@ Example of response
"started_at": "2015-12-24T17:54:24.729Z",
"finished_at": "2015-12-24T17:54:24.921Z",
"duration": 0.192,
"queued_duration": 0.023,
"artifacts_expire_at": "2016-01-23T17:54:24.921Z",
"tag_list": [
"docker runner", "win10-2004"
......@@ -187,6 +189,7 @@ Example of response
"started_at": "2015-12-24T17:54:24.729Z",
"finished_at": "2015-12-24T17:54:24.921Z",
"duration": 0.192,
"queued_duration": 0.023,
"artifacts_expire_at": "2016-01-23T17:54:24.921Z",
"tag_list": [
"docker runner", "ubuntu18"
......@@ -241,6 +244,7 @@ Example of response
"started_at": "2015-12-24T17:54:27.722Z",
"finished_at": "2015-12-24T17:54:27.895Z",
"duration": 0.173,
"queued_duration": 0.023,
"artifacts_file": {
"filename": "artifacts.zip",
"size": 1000
......@@ -339,6 +343,7 @@ Example of response
"started_at": "2015-12-24T17:54:27.722Z",
"finished_at": "2015-12-24T17:58:27.895Z",
"duration": 240,
"queued_duration": 0.123,
"id": 7,
"name": "teaspoon",
"pipeline": {
......@@ -422,6 +427,7 @@ Example of response
"started_at": "2015-12-24T17:54:30.733Z",
"finished_at": "2015-12-24T17:54:31.198Z",
"duration": 0.465,
"queued_duration": 0.123,
"artifacts_expire_at": "2016-01-23T17:54:31.198Z",
"id": 8,
"name": "rubocop",
......@@ -575,6 +581,7 @@ Example of response
"started_at": "2015-12-24T17:54:30.733Z",
"finished_at": "2015-12-24T17:54:31.198Z",
"duration": 0.465,
"queued_duration": 0.010,
"artifacts_expire_at": "2016-01-23T17:54:31.198Z",
"tag_list": [
"docker runner", "macos-10.15"
......@@ -675,6 +682,7 @@ Example of response
"started_at": "2016-01-11T10:14:09.526Z",
"finished_at": null,
"duration": 8,
"queued_duration": 0.010,
"id": 42,
"name": "rubocop",
"ref": "master",
......@@ -724,6 +732,7 @@ Example of response
"started_at": null,
"finished_at": null,
"duration": null,
"queued_duration": 0.010,
"id": 42,
"name": "rubocop",
"ref": "master",
......@@ -784,6 +793,7 @@ Example of response
"started_at": "2016-01-11T10:13:33.506Z",
"finished_at": "2016-01-11T10:15:10.506Z",
"duration": 97.0,
"queued_duration": 0.010,
"status": "failed",
"tag": false,
"web_url": "https://example.com/foo/bar/-/jobs/42",
......@@ -827,13 +837,14 @@ Example of response
"started_at": null,
"finished_at": null,
"duration": null,
"queued_duration": 0.010,
"id": 42,
"name": "rubocop",
"ref": "master",
"artifacts": [],
"runner": null,
"stage": "test",
"status": "started",
"status": "pending",
"tag": false,
"web_url": "https://example.com/foo/bar/-/jobs/42",
"user": null
......
......@@ -117,7 +117,8 @@ Example of response
"started_at": null,
"finished_at": "2016-08-11T11:32:35.145Z",
"committed_at": null,
"duration": null,
"duration": 123.65,
"queued_duration": 0.010,
"coverage": "30.0",
"web_url": "https://example.com/foo/bar/pipelines/46"
}
......@@ -254,6 +255,7 @@ Example of response
"finished_at": null,
"committed_at": null,
"duration": null,
"queued_duration": 0.010,
"coverage": null,
"web_url": "https://example.com/foo/bar/pipelines/61"
}
......@@ -302,6 +304,7 @@ Response:
"finished_at": "2016-08-11T11:32:35.145Z",
"committed_at": null,
"duration": null,
"queued_duration": 0.010,
"coverage": null,
"web_url": "https://example.com/foo/bar/pipelines/46"
}
......@@ -350,6 +353,7 @@ Response:
"finished_at": "2016-08-11T11:32:35.145Z",
"committed_at": null,
"duration": null,
"queued_duration": 0.010,
"coverage": null,
"web_url": "https://example.com/foo/bar/pipelines/46"
}
......
......@@ -166,6 +166,8 @@ Each file is expected to have its own primary ID and model. Geo strongly recomme
To implement Geo replication of a new blob-type Model, [open an issue with the provided issue template](https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Geo%20Replicate%20a%20new%20blob%20type).
To view the implementation steps without opening an issue, [view the issue template file](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Geo%20Replicate%20a%20new%20blob%20type.md).
### Repository Replicator Strategy
Models that refer to any Git repository on disk are supported by Geo with the `Geo::RepositoryReplicatorStrategy` module. For example, see how [Geo replication was implemented for Group-level Wikis](https://gitlab.com/gitlab-org/gitlab/-/issues/208147). Note that this issue does not implement verification, since verification of Git repositories was not yet added to the Geo self-service framework. An example implementing verification can be found in the merge request to [Add Snippet repository verification](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56596).
......@@ -173,3 +175,5 @@ Models that refer to any Git repository on disk are supported by Geo with the `G
Each Git repository is expected to have its own primary ID and model.
To implement Geo replication of a new Git repository-type Model, [open an issue with the provided issue template](https://gitlab.com/gitlab-org/gitlab/-/issues/new?issuable_template=Geo%20Replicate%20a%20new%20Git%20repository%20type).
To view the implementation steps without opening an issue, [view the issue template file](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Geo%20Replicate%20a%20new%20Git%20repository%20type.md).
......@@ -51,15 +51,14 @@ describe('GeoNodesBetaApp', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findGeoNodesBetaContainer = () => wrapper.find('section');
const findGeoLearnMoreLink = () => wrapper.find(GlLink);
const findGeoAddSiteButton = () => wrapper.find(GlButton);
const findGlLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findGeoEmptyState = () => wrapper.find(GeoNodesEmptyState);
const findGeoNodes = () => wrapper.findAll(GeoNodes);
const findGeoLearnMoreLink = () => wrapper.findComponent(GlLink);
const findGeoAddSiteButton = () => wrapper.findComponent(GlButton);
const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findGeoEmptyState = () => wrapper.findComponent(GeoNodesEmptyState);
const findGeoNodes = () => wrapper.findAllComponents(GeoNodes);
describe('template', () => {
describe('always', () => {
......@@ -91,19 +90,19 @@ describe('GeoNodesBetaApp', () => {
});
describe(`when isLoading is ${isLoading} & nodes length ${nodes.length}`, () => {
it(`does ${!showLoadingIcon ? 'not ' : ''}render GlLoadingIcon`, () => {
it(`does ${showLoadingIcon ? '' : 'not '}render GlLoadingIcon`, () => {
expect(findGlLoadingIcon().exists()).toBe(showLoadingIcon);
});
it(`does ${!showNodes ? 'not ' : ''}render GeoNodes`, () => {
it(`does ${showNodes ? '' : 'not '}render GeoNodes`, () => {
expect(findGeoNodes().exists()).toBe(showNodes);
});
it(`does ${!showEmptyState ? 'not ' : ''}render EmptyState`, () => {
it(`does ${showEmptyState ? '' : 'not '}render EmptyState`, () => {
expect(findGeoEmptyState().exists()).toBe(showEmptyState);
});
it(`does ${!showAddButton ? 'not ' : ''}render AddSiteButton`, () => {
it(`does ${showAddButton ? '' : 'not '}render AddSiteButton`, () => {
expect(findGeoAddSiteButton().exists()).toBe(showAddButton);
});
});
......
......@@ -37,10 +37,9 @@ describe('GeoNodesEmptyState', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findGeoEmptyState = () => wrapper.find(GlEmptyState);
const findGeoEmptyState = () => wrapper.findComponent(GlEmptyState);
describe('template', () => {
beforeEach(() => {
......
......@@ -22,7 +22,6 @@ describe('GeoNodes', () => {
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findGeoNodesContainer = () => wrapper.find('div');
......
......@@ -7,6 +7,7 @@ import {
MOCK_PRIMARY_VERSION,
MOCK_REPLICABLE_TYPES,
} from 'ee_jest/geo_nodes_beta/mock_data';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
const localVue = createLocalVue();
localVue.use(Vuex);
......@@ -28,23 +29,24 @@ describe('GeoNodeActionsDesktop', () => {
},
});
wrapper = shallowMount(GeoNodeActionsDesktop, {
localVue,
store,
propsData: {
...defaultProps,
...props,
},
});
wrapper = extendedWrapper(
shallowMount(GeoNodeActionsDesktop, {
localVue,
store,
propsData: {
...defaultProps,
...props,
},
}),
);
};
afterEach(() => {
wrapper.destroy();
});
const findGeoDesktopActionsButtons = () => wrapper.findAll(GlButton);
const findGeoDesktopActionsRemoveButton = () =>
wrapper.find('[data-testid="geo-desktop-remove-action"]');
const findGeoDesktopActionsButtons = () => wrapper.findAllComponents(GlButton);
const findGeoDesktopActionsRemoveButton = () => wrapper.findByTestId('geo-desktop-remove-action');
describe('template', () => {
describe('always', () => {
......
......@@ -7,6 +7,7 @@ import {
MOCK_PRIMARY_VERSION,
MOCK_REPLICABLE_TYPES,
} from 'ee_jest/geo_nodes_beta/mock_data';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
const localVue = createLocalVue();
localVue.use(Vuex);
......@@ -28,24 +29,26 @@ describe('GeoNodeActionsMobile', () => {
},
});
wrapper = shallowMount(GeoNodeActionsMobile, {
localVue,
store,
propsData: {
...defaultProps,
...props,
},
});
wrapper = extendedWrapper(
shallowMount(GeoNodeActionsMobile, {
localVue,
store,
propsData: {
...defaultProps,
...props,
},
}),
);
};
afterEach(() => {
wrapper.destroy();
});
const findGeoMobileActionsDropdown = () => wrapper.find(GlDropdown);
const findGeoMobileActionsDropdownItems = () => wrapper.findAll(GlDropdownItem);
const findGeoMobileActionsDropdown = () => wrapper.findComponent(GlDropdown);
const findGeoMobileActionsDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
const findGeoMobileActionsRemoveDropdownItem = () =>
wrapper.find('[data-testid="geo-mobile-remove-action"]');
wrapper.findByTestId('geo-mobile-remove-action');
describe('template', () => {
describe('always', () => {
......
......@@ -43,8 +43,8 @@ describe('GeoNodeActions', () => {
wrapper.destroy();
});
const findGeoMobileActions = () => wrapper.find(GeoNodeActionsMobile);
const findGeoDesktopActions = () => wrapper.find(GeoNodeActionsDesktop);
const findGeoMobileActions = () => wrapper.findComponent(GeoNodeActionsMobile);
const findGeoDesktopActions = () => wrapper.findComponent(GeoNodeActionsDesktop);
describe('template', () => {
beforeEach(() => {
......
......@@ -46,11 +46,11 @@ describe('GeoNodeHeader', () => {
wrapper.destroy();
});
const findHeaderCollapseButton = () => wrapper.find(GlButton);
const findCurrentNodeBadge = () => wrapper.find(GlBadge);
const findGeoNodeHealthStatus = () => wrapper.find(GeoNodeHealthStatus);
const findGeoNodeLastUpdated = () => wrapper.find(GeoNodeLastUpdated);
const findGeoNodeActions = () => wrapper.find(GeoNodeActions);
const findHeaderCollapseButton = () => wrapper.findComponent(GlButton);
const findCurrentNodeBadge = () => wrapper.findComponent(GlBadge);
const findGeoNodeHealthStatus = () => wrapper.findComponent(GeoNodeHealthStatus);
const findGeoNodeLastUpdated = () => wrapper.findComponent(GeoNodeLastUpdated);
const findGeoNodeActions = () => wrapper.findComponent(GeoNodeActions);
describe('template', () => {
describe('always', () => {
......
......@@ -39,8 +39,8 @@ describe('GeoNodeHealthStatus', () => {
wrapper.destroy();
});
const findGeoStatusBadge = () => wrapper.find(GlBadge);
const findGeoStatusIcon = () => wrapper.find(GlIcon);
const findGeoStatusBadge = () => wrapper.findComponent(GlBadge);
const findGeoStatusIcon = () => wrapper.findComponent(GlIcon);
const findGeoStatusText = () => wrapper.find('span');
describe.each`
......
......@@ -8,6 +8,7 @@ import {
STATUS_DELAY_THRESHOLD_MS,
} from 'ee/geo_nodes_beta/constants';
import { MOCK_PRIMARY_VERSION, MOCK_REPLICABLE_TYPES } from 'ee_jest/geo_nodes_beta/mock_data';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { differenceInMilliseconds } from '~/lib/utils/datetime_utility';
const localVue = createLocalVue();
......@@ -34,25 +35,27 @@ describe('GeoNodeLastUpdated', () => {
},
});
wrapper = shallowMount(GeoNodeLastUpdated, {
localVue,
store,
propsData: {
...defaultProps,
...props,
},
});
wrapper = extendedWrapper(
shallowMount(GeoNodeLastUpdated, {
localVue,
store,
propsData: {
...defaultProps,
...props,
},
}),
);
};
afterEach(() => {
wrapper.destroy();
});
const findMainText = () => wrapper.find('[data-testid="last-updated-main-text"]');
const findGlIcon = () => wrapper.find(GlIcon);
const findGlPopover = () => wrapper.find(GlPopover);
const findMainText = () => wrapper.findByTestId('last-updated-main-text');
const findGlIcon = () => wrapper.findComponent(GlIcon);
const findGlPopover = () => wrapper.findComponent(GlPopover);
const findPopoverText = () => findGlPopover().find('p');
const findPopoverLink = () => findGlPopover().find(GlLink);
const findPopoverLink = () => findGlPopover().findComponent(GlLink);
describe('template', () => {
describe('always', () => {
......@@ -75,12 +78,12 @@ describe('GeoNodeLastUpdated', () => {
});
it('renders the popover text correctly', () => {
expect(findPopoverText().exists()).toBeTruthy();
expect(findPopoverText().exists()).toBe(true);
expect(findPopoverText().text()).toBe("Node's status was updated 10 minutes ago.");
});
it('renders the popover link always', () => {
expect(findPopoverLink().exists()).toBeTruthy();
expect(findPopoverLink().exists()).toBe(true);
});
});
......
......@@ -6,7 +6,10 @@ module API
class JobBasic < Grape::Entity
expose :id, :status, :stage, :name, :ref, :tag, :coverage, :allow_failure
expose :created_at, :started_at, :finished_at
expose :duration
expose :duration,
documentation: { type: 'Floating', desc: 'Time spent running' }
expose :queued_duration,
documentation: { type: 'Floating', desc: 'Time spent enqueued' }
expose :user, with: ::API::Entities::User
expose :commit, with: ::API::Entities::Commit
expose :pipeline, with: ::API::Entities::Ci::PipelineBasic
......
......@@ -9,6 +9,7 @@ module API
expose :user, with: Entities::UserBasic
expose :created_at, :updated_at, :started_at, :finished_at, :committed_at
expose :duration
expose :queued_duration
expose :coverage
expose :detailed_status, using: DetailedStatusEntity do |pipeline, options|
pipeline.detailed_status(options[:current_user])
......
......@@ -30,6 +30,7 @@ module Gitlab
build_started_at: build.started_at,
build_finished_at: build.finished_at,
build_duration: build.duration,
build_queued_duration: build.queued_duration,
build_allow_failure: build.allow_failure,
build_failure_reason: build.failure_reason,
pipeline_id: commit.id,
......
......@@ -31,6 +31,7 @@ module Gitlab
created_at: pipeline.created_at,
finished_at: pipeline.finished_at,
duration: pipeline.duration,
queued_duration: pipeline.queued_duration,
variables: pipeline.variables.map(&:hook_attrs)
}
end
......@@ -59,6 +60,8 @@ module Gitlab
created_at: build.created_at,
started_at: build.started_at,
finished_at: build.finished_at,
duration: build.duration,
queued_duration: build.queued_duration,
when: build.when,
manual: build.action?,
allow_failure: build.allow_failure,
......
......@@ -12,6 +12,7 @@
"started_at",
"finished_at",
"duration",
"queued_duration",
"user",
"commit",
"pipeline",
......@@ -34,6 +35,7 @@
"started_at": { "type": ["null", "string"] },
"finished_at": { "type": ["null", "string"] },
"duration": { "type": ["null", "number"] },
"queued_duration": { "type": ["null", "number"] },
"user": { "$ref": "user/basic.json" },
"commit": {
"oneOf": [
......
......@@ -121,14 +121,16 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
all_releases_query_path = 'releases/queries/all_releases.query.graphql'
one_release_query_path = 'releases/queries/one_release.query.graphql'
fragment_paths = ['releases/queries/release.fragment.graphql']
one_release_for_editing_query_path = 'releases/queries/one_release_for_editing.query.graphql'
release_fragment_path = 'releases/queries/release.fragment.graphql'
release_for_editing_fragment_path = 'releases/queries/release_for_editing.fragment.graphql'
before(:all) do
clean_frontend_fixtures('graphql/releases/')
end
it "graphql/#{all_releases_query_path}.json" do
query = get_graphql_query_as_string(all_releases_query_path, fragment_paths)
query = get_graphql_query_as_string(all_releases_query_path, [release_fragment_path])
post_graphql(query, current_user: admin, variables: { fullPath: project.full_path })
......@@ -136,7 +138,15 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
end
it "graphql/#{one_release_query_path}.json" do
query = get_graphql_query_as_string(one_release_query_path, fragment_paths)
query = get_graphql_query_as_string(one_release_query_path, [release_fragment_path])
post_graphql(query, current_user: admin, variables: { fullPath: project.full_path, tagName: release.tag })
expect_graphql_errors_to_be_empty
end
it "graphql/#{one_release_for_editing_query_path}.json" do
query = get_graphql_query_as_string(one_release_for_editing_query_path, [release_for_editing_fragment_path])
post_graphql(query, current_user: admin, variables: { fullPath: project.full_path, tagName: release.tag })
......
......@@ -129,6 +129,68 @@ Object {
}
`;
exports[`releases/util.js convertOneReleaseForEditingGraphQLResponse matches snapshot 1`] = `
Object {
"data": Object {
"_links": Object {
"self": "http://localhost/releases-namespace/releases-project/-/releases/v1.1",
"selfUrl": "http://localhost/releases-namespace/releases-project/-/releases/v1.1",
},
"assets": Object {
"count": undefined,
"links": Array [
Object {
"id": "gid://gitlab/Releases::Link/13",
"linkType": "image",
"name": "Image",
"url": "https://example.com/image",
},
Object {
"id": "gid://gitlab/Releases::Link/12",
"linkType": "package",
"name": "Package",
"url": "https://example.com/package",
},
Object {
"id": "gid://gitlab/Releases::Link/11",
"linkType": "runbook",
"name": "Runbook",
"url": "http://localhost/releases-namespace/releases-project/runbook",
},
Object {
"id": "gid://gitlab/Releases::Link/10",
"linkType": "other",
"name": "linux-amd64 binaries",
"url": "https://downloads.example.com/bin/gitlab-linux-amd64",
},
],
"sources": Array [],
},
"author": undefined,
"description": "Best. Release. **Ever.** :rocket:",
"evidences": Array [],
"milestones": Array [
Object {
"issueStats": Object {},
"stats": undefined,
"title": "12.3",
"webPath": undefined,
"webUrl": undefined,
},
Object {
"issueStats": Object {},
"stats": undefined,
"title": "12.4",
"webPath": undefined,
"webUrl": undefined,
},
],
"name": "The first release",
"tagName": "v1.1",
},
}
`;
exports[`releases/util.js convertOneReleaseGraphQLResponse matches snapshot 1`] = `
Object {
"data": Object {
......
......@@ -14,6 +14,9 @@ const originalAllReleasesQueryResponse = getJSONFixture(
const originalOneReleaseQueryResponse = getJSONFixture(
'graphql/releases/queries/one_release.query.graphql.json',
);
const originalOneReleaseForEditingQueryResponse = getJSONFixture(
'graphql/releases/queries/one_release_for_editing.query.graphql.json',
);
describe('releases/util.js', () => {
describe('releaseToApiJson', () => {
......@@ -135,6 +138,26 @@ describe('releases/util.js', () => {
expect(convertedRelease.assets.links[0].linkType).toBeUndefined();
});
it('handles assets that have no links', () => {
expect(convertedRelease.assets.links[0]).not.toBeUndefined();
delete releaseFromResponse.assets.links;
convertedRelease = convertGraphQLRelease(releaseFromResponse);
expect(convertedRelease.assets.links).toEqual([]);
});
it('handles assets that have no sources', () => {
expect(convertedRelease.assets.sources[0]).not.toBeUndefined();
delete releaseFromResponse.assets.sources;
convertedRelease = convertGraphQLRelease(releaseFromResponse);
expect(convertedRelease.assets.sources).toEqual([]);
});
});
describe('_links', () => {
......@@ -160,6 +183,33 @@ describe('releases/util.js', () => {
expect(convertedRelease.commit).toBeUndefined();
});
});
describe('milestones', () => {
it("handles releases that don't have any milestone stats", () => {
expect(convertedRelease.milestones[0].issueStats).not.toBeUndefined();
releaseFromResponse.milestones.nodes = releaseFromResponse.milestones.nodes.map((n) => ({
...n,
stats: undefined,
}));
convertedRelease = convertGraphQLRelease(releaseFromResponse);
expect(convertedRelease.milestones[0].issueStats).toEqual({});
});
});
describe('evidences', () => {
it("handles releases that don't have any evidences", () => {
expect(convertedRelease.evidences).not.toBeUndefined();
delete releaseFromResponse.evidences;
convertedRelease = convertGraphQLRelease(releaseFromResponse);
expect(convertedRelease.evidences).toEqual([]);
});
});
});
describe('convertAllReleasesGraphQLResponse', () => {
......@@ -173,4 +223,12 @@ describe('releases/util.js', () => {
expect(convertOneReleaseGraphQLResponse(originalOneReleaseQueryResponse)).toMatchSnapshot();
});
});
describe('convertOneReleaseForEditingGraphQLResponse', () => {
it('matches snapshot', () => {
expect(
convertOneReleaseGraphQLResponse(originalOneReleaseForEditingQueryResponse),
).toMatchSnapshot();
});
});
});
......@@ -26,6 +26,7 @@ RSpec.describe Types::Ci::JobType do
pipeline
playable
queued_at
queued_duration
refName
refPath
retryable
......
......@@ -9,7 +9,8 @@ RSpec.describe Types::Ci::PipelineType do
it 'contains attributes related to a pipeline' do
expected_fields = %w[
id iid sha before_sha status detailed_status config_source duration
id iid sha before_sha status detailed_status config_source
duration queued_duration
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
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['Duration'] do
let(:duration) { 17.minutes }
it 'presents information as a floating point number' do
expect(described_class.coerce_isolated_result(duration)).to eq(duration.to_f)
end
it 'accepts integers as input' do
expect(described_class.coerce_isolated_input(100)).to eq(100.0)
end
it 'accepts floats as input' do
expect(described_class.coerce_isolated_input(0.5)).to eq(0.5)
end
it 'rejects invalid input' do
expect { described_class.coerce_isolated_input('not valid') }
.to raise_error(GraphQL::CoercionError)
end
it 'rejects nil' do
expect { described_class.coerce_isolated_input(nil) }
.to raise_error(GraphQL::CoercionError)
end
end
......@@ -9,6 +9,10 @@ RSpec.describe Gitlab::DataBuilder::Build do
let(:build) { create(:ci_build, :running, runner: runner, user: user) }
describe '.build' do
around do |example|
travel_to(Time.current) { example.run }
end
let(:data) do
described_class.build(build)
end
......@@ -22,6 +26,8 @@ RSpec.describe Gitlab::DataBuilder::Build do
it { expect(data[:build_created_at]).to eq(build.created_at) }
it { expect(data[:build_started_at]).to eq(build.started_at) }
it { expect(data[:build_finished_at]).to eq(build.finished_at) }
it { expect(data[:build_duration]).to eq(build.duration) }
it { expect(data[:build_queued_duration]).to eq(build.queued_duration) }
it { expect(data[:build_allow_failure]).to eq(false) }
it { expect(data[:build_failure_reason]).to eq(build.failure_reason) }
it { expect(data[:project_id]).to eq(build.project.id) }
......
......@@ -4679,25 +4679,30 @@ RSpec.describe Ci::Build do
end
describe '#execute_hooks' do
before do
build.clear_memoization(:build_data)
end
context 'with project hooks' do
let(:build_data) { double(:BuildData, dup: double(:DupedData)) }
before do
create(:project_hook, project: project, job_events: true)
end
it 'execute hooks' do
expect_any_instance_of(ProjectHook).to receive(:async_execute)
it 'calls project.execute_hooks(build_data, :job_hooks)' do
expect(::Gitlab::DataBuilder::Build)
.to receive(:build).with(build).and_return(build_data)
expect(build.project)
.to receive(:execute_hooks).with(build_data.dup, :job_hooks)
build.execute_hooks
end
end
context 'without relevant project hooks' do
before do
create(:project_hook, project: project, job_events: false)
end
it 'does not execute a hook' do
expect_any_instance_of(ProjectHook).not_to receive(:async_execute)
context 'without project hooks' do
it 'does not call project.execute_hooks' do
expect(build.project).not_to receive(:execute_hooks)
build.execute_hooks
end
......@@ -4708,8 +4713,10 @@ RSpec.describe Ci::Build do
create(:service, active: true, job_events: true, project: project)
end
it 'execute services' do
expect_any_instance_of(Service).to receive(:async_execute)
it 'executes services' do
allow_next_found_instance_of(Service) do |service|
expect(service).to receive(:async_execute)
end
build.execute_hooks
end
......@@ -4720,8 +4727,10 @@ RSpec.describe Ci::Build do
create(:service, active: true, job_events: false, project: project)
end
it 'execute services' do
expect_any_instance_of(Service).not_to receive(:async_execute)
it 'does not execute services' do
allow_next_found_instance_of(Service) do |service|
expect(service).not_to receive(:async_execute)
end
build.execute_hooks
end
......
......@@ -259,6 +259,40 @@ RSpec.describe CommitStatus do
end
end
describe '#queued_duration' do
subject { commit_status.queued_duration }
around do |example|
travel_to(Time.current) { example.run }
end
context 'when created, then enqueued, then started' do
before do
commit_status.queued_at = 30.seconds.ago
commit_status.started_at = 25.seconds.ago
end
it { is_expected.to eq(5.0) }
end
context 'when created but not yet enqueued' do
before do
commit_status.queued_at = nil
end
it { is_expected.to be_nil }
end
context 'when enqueued, but not started' do
before do
commit_status.queued_at = Time.current - 1.minute
commit_status.started_at = nil
end
it { is_expected.to eq(1.minute) }
end
end
describe '.latest' do
subject { described_class.latest.order(:id) }
......
......@@ -362,6 +362,25 @@ RSpec.describe API::Ci::Pipelines do
it do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
expect(json_response).to all match a_hash_including(
'duration' => be_nil,
'queued_duration' => (be >= 0.0)
)
end
end
context 'when filtering to only running jobs' do
let(:query) { { 'scope' => 'running' } }
it do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an Array
expect(json_response).to all match a_hash_including(
'duration' => (be >= 0.0),
'queued_duration' => (be >= 0.0)
)
end
end
......
......@@ -5,6 +5,10 @@ require 'spec_helper'
RSpec.describe 'Query.project(fullPath).pipelines.job(id)' do
include GraphqlHelpers
around do |example|
travel_to(Time.current) { example.run }
end
let_it_be(:user) { create_default(:user) }
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:pipeline) { create(:ci_pipeline, project: project) }
......@@ -35,13 +39,20 @@ RSpec.describe 'Query.project(fullPath).pipelines.job(id)' do
let(:terminal_type) { 'CiJob' }
it 'retrieves scalar fields' do
job_2.update!(
created_at: 40.seconds.ago,
queued_at: 32.seconds.ago,
started_at: 30.seconds.ago,
finished_at: 5.seconds.ago
)
post_graphql(query, current_user: user)
expect(graphql_data_at(*path)).to match a_hash_including(
'id' => global_id_of(job_2),
'name' => job_2.name,
'allowFailure' => job_2.allow_failure,
'duration' => job_2.duration,
'duration' => 25,
'queuedDuration' => 2.0,
'status' => job_2.status.upcase
)
end
......
......@@ -8,6 +8,49 @@ RSpec.describe 'Query.project(fullPath).pipelines' do
let_it_be(:project) { create(:project, :repository, :public) }
let_it_be(:user) { create(:user) }
around do |example|
travel_to(Time.current) { example.run }
end
describe 'duration fields' do
let_it_be(:pipeline) do
create(:ci_pipeline, project: project)
end
let(:query_path) do
[
[:project, { full_path: project.full_path }],
[:pipelines],
[:nodes]
]
end
let(:query) do
wrap_fields(query_graphql_path(query_path, 'queuedDuration duration'))
end
before do
pipeline.update!(
created_at: 1.minute.ago,
started_at: 55.seconds.ago
)
create(:ci_build, :success,
pipeline: pipeline,
started_at: 55.seconds.ago,
finished_at: 10.seconds.ago)
pipeline.update_duration
pipeline.save!
post_graphql(query, current_user: user)
end
it 'includes the duration fields' do
path = query_path.map(&:first)
expect(graphql_data_at(*path, :queued_duration)).to eq [5.0]
expect(graphql_data_at(*path, :duration)).to eq [45]
end
end
describe '.jobs' do
let(:first_n) { var('Int') }
let(:query_path) do
......
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