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
This diff is collapsed.
......@@ -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