提交 2164573e 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 f3e7bc80
......@@ -51,6 +51,7 @@ const Api = {
pipelinesPath: '/api/:version/projects/:id/pipelines/',
environmentsPath: '/api/:version/projects/:id/environments',
rawFilePath: '/api/:version/projects/:id/repository/files/:path/raw',
issuePath: '/api/:version/projects/:id/issues/:issue_iid',
group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
......@@ -540,6 +541,22 @@ const Api = {
return axios.get(url, { params });
},
updateIssue(project, issue, data = {}) {
const url = Api.buildUrl(Api.issuePath)
.replace(':id', encodeURIComponent(project))
.replace(':issue_iid', encodeURIComponent(issue));
return axios.put(url, data);
},
updateMergeRequest(project, mergeRequest, data = {}) {
const url = Api.buildUrl(Api.projectMergeRequestPath)
.replace(':id', encodeURIComponent(project))
.replace(':mrid', encodeURIComponent(mergeRequest));
return axios.put(url, data);
},
buildUrl(url) {
return joinPaths(gon.relative_url_root || '', url.replace(':version', gon.api_version));
},
......
......@@ -47,7 +47,7 @@ export default {
'btn-transparent comment-indicator': isNewNote,
'js-image-badge badge badge-pill': !isNewNote,
}"
class="position-absolute"
class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center"
type="button"
@mousedown="$emit('mousedown', $event)"
@mouseup="$emit('mouseup', $event)"
......
......@@ -4,7 +4,7 @@ import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import DagGraph from './dag_graph.vue';
import { DEFAULT, PARSE_FAILURE, LOAD_FAILURE, UNSUPPORTED_DATA } from './constants';
import { parseData } from './utils';
import { parseData } from './parsing_utils';
export default {
// eslint-disable-next-line @gitlab/require-i18n-strings
......
......@@ -3,7 +3,8 @@ import * as d3 from 'd3';
import { uniqueId } from 'lodash';
import { PARSE_FAILURE } from './constants';
import { createSankey, getMaxNodes, removeOrphanNodes } from './utils';
import { getMaxNodes, removeOrphanNodes } from './parsing_utils';
import { calculateClip, createLinkPath, createSankey, labelPosition } from './drawing_utils';
export default {
viewOptions: {
......@@ -78,7 +79,7 @@ export default {
return (
link
.append('path')
.attr('d', this.createLinkPath)
.attr('d', (d, i) => createLinkPath(d, i, this.$options.viewOptions.nodeWidth))
.attr('stroke', ({ gradId }) => `url(#${gradId})`)
.style('stroke-linejoin', 'round')
// minus two to account for the rounded nodes
......@@ -89,7 +90,10 @@ export default {
appendLabelAsForeignObject(d, i, n) {
const currentNode = n[i];
const { height, wrapperWidth, width, x, y, textAlign } = this.labelPosition(d);
const { height, wrapperWidth, width, x, y, textAlign } = labelPosition(d, {
...this.$options.viewOptions,
width: this.width,
});
const labelClasses = [
'gl-display-flex',
......@@ -128,44 +132,13 @@ export default {
},
createClip(link) {
/*
Because large link values can overrun their box, we create a clip path
to trim off the excess in charts that have few nodes per column and are
therefore tall.
The box is created by
M: moving to outside midpoint of the source node
V: drawing a vertical line to maximum of the bottom link edge or
the lowest edge of the node (can be d.y0 or d.y1 depending on the link's path)
H: drawing a horizontal line to the outside edge of the destination node
V: drawing a vertical line back up to the minimum of the top link edge or
the highest edge of the node (can be d.y0 or d.y1 depending on the link's path)
H: drawing a horizontal line back to the outside edge of the source node
Z: closing the path, back to the start point
*/
const clip = ({ y0, y1, source, target, width }) => {
const bottomLinkEdge = Math.max(y1, y0) + width / 2;
const topLinkEdge = Math.min(y0, y1) - width / 2;
/* eslint-disable @gitlab/require-i18n-strings */
return `
M${source.x0}, ${y1}
V${Math.max(bottomLinkEdge, y0, y1)}
H${target.x1}
V${Math.min(topLinkEdge, y0, y1)}
H${source.x0}
Z`;
/* eslint-enable @gitlab/require-i18n-strings */
};
return link
.append('clipPath')
.attr('id', d => {
return this.createAndAssignId(d, 'clipId', 'dag-clip');
})
.append('path')
.attr('d', clip);
.attr('d', calculateClip);
},
createGradient(link) {
......@@ -189,44 +162,6 @@ export default {
.attr('stop-color', ({ target }) => this.color(target));
},
createLinkPath({ y0, y1, source, target, width }, idx) {
const { nodeWidth } = this.$options.viewOptions;
/*
Creates a series of staggered midpoints for the link paths, so they
don't run along one channel and can be distinguished.
First, get a point staggered by index and link width, modulated by the link box
to find a point roughly between the nodes.
Then offset it by nodeWidth, so it doesn't run under any nodes at the left.
Determine where it would overlap at the right.
Finally, select the leftmost of these options:
- offset from the source node based on index + fudge;
- a fuzzy offset from the right node, using Math.random adds a little blur
- a hard offset from the end node, if random pushes it over
Then draw a line from the start node to the bottom-most point of the midline
up to the topmost point in that line and then to the middle of the end node
*/
const xValRaw = source.x1 + (((idx + 1) * width) % (target.x1 - source.x0));
const xValMin = xValRaw + nodeWidth;
const overlapPoint = source.x1 + (target.x0 - source.x1);
const xValMax = overlapPoint - nodeWidth * 1.4;
const midPointX = Math.min(xValMin, target.x0 - nodeWidth * 4 * Math.random(), xValMax);
return d3.line()([
[(source.x0 + source.x1) / 2, y0],
[midPointX, y0],
[midPointX, y1],
[(target.x0 + target.x1) / 2, y1],
]);
},
createLinks(svg, linksData) {
const link = this.generateLinks(svg, linksData);
this.createGradient(link);
......@@ -322,42 +257,6 @@ export default {
return ({ name }) => colorFn(name);
},
labelPosition({ x0, x1, y0, y1 }) {
const { paddingForLabels, labelMargin, nodePadding } = this.$options.viewOptions;
const firstCol = x0 <= paddingForLabels;
const lastCol = x1 >= this.width - paddingForLabels;
if (firstCol) {
return {
x: 0 + labelMargin,
y: y0,
height: `${y1 - y0}px`,
width: paddingForLabels - 2 * labelMargin,
textAlign: 'right',
};
}
if (lastCol) {
return {
x: this.width - paddingForLabels + labelMargin,
y: y0,
height: `${y1 - y0}px`,
width: paddingForLabels - 2 * labelMargin,
textAlign: 'left',
};
}
return {
x: (x1 + x0) / 2,
y: y0 - nodePadding,
height: `${nodePadding}px`,
width: 'max-content',
wrapperWidth: paddingForLabels - 2 * labelMargin,
textAlign: x0 < this.width / 2 ? 'left' : 'right',
};
},
transformData(parsed) {
const baseLayout = createSankey()(parsed);
const cleanedNodes = removeOrphanNodes(baseLayout.nodes);
......
import * as d3 from 'd3';
import { sankey, sankeyLeft } from 'd3-sankey';
export const calculateClip = ({ y0, y1, source, target, width }) => {
/*
Because large link values can overrun their box, we create a clip path
to trim off the excess in charts that have few nodes per column and are
therefore tall.
The box is created by
M: moving to outside midpoint of the source node
V: drawing a vertical line to maximum of the bottom link edge or
the lowest edge of the node (can be d.y0 or d.y1 depending on the link's path)
H: drawing a horizontal line to the outside edge of the destination node
V: drawing a vertical line back up to the minimum of the top link edge or
the highest edge of the node (can be d.y0 or d.y1 depending on the link's path)
H: drawing a horizontal line back to the outside edge of the source node
Z: closing the path, back to the start point
*/
const bottomLinkEdge = Math.max(y1, y0) + width / 2;
const topLinkEdge = Math.min(y0, y1) - width / 2;
/* eslint-disable @gitlab/require-i18n-strings */
return `
M${source.x0}, ${y1}
V${Math.max(bottomLinkEdge, y0, y1)}
H${target.x1}
V${Math.min(topLinkEdge, y0, y1)}
H${source.x0}
Z
`;
/* eslint-enable @gitlab/require-i18n-strings */
};
export const createLinkPath = ({ y0, y1, source, target, width }, idx, nodeWidth) => {
/*
Creates a series of staggered midpoints for the link paths, so they
don't run along one channel and can be distinguished.
First, get a point staggered by index and link width, modulated by the link box
to find a point roughly between the nodes.
Then offset it by nodeWidth, so it doesn't run under any nodes at the left.
Determine where it would overlap at the right.
Finally, select the leftmost of these options:
- offset from the source node based on index + fudge;
- a fuzzy offset from the right node, using Math.random adds a little blur
- a hard offset from the end node, if random pushes it over
Then draw a line from the start node to the bottom-most point of the midline
up to the topmost point in that line and then to the middle of the end node
*/
const xValRaw = source.x1 + (((idx + 1) * width) % (target.x1 - source.x0));
const xValMin = xValRaw + nodeWidth;
const overlapPoint = source.x1 + (target.x0 - source.x1);
const xValMax = overlapPoint - nodeWidth * 1.4;
const midPointX = Math.min(xValMin, target.x0 - nodeWidth * 4 * Math.random(), xValMax);
return d3.line()([
[(source.x0 + source.x1) / 2, y0],
[midPointX, y0],
[midPointX, y1],
[(target.x0 + target.x1) / 2, y1],
]);
};
/*
createSankey calls the d3 layout to generate the relationships and positioning
values for the nodes and links in the graph.
*/
export const createSankey = ({
width = 10,
height = 10,
nodeWidth = 10,
nodePadding = 10,
paddingForLabels = 1,
} = {}) => {
const sankeyGenerator = sankey()
.nodeId(({ name }) => name)
.nodeAlign(sankeyLeft)
.nodeWidth(nodeWidth)
.nodePadding(nodePadding)
.extent([
[paddingForLabels, paddingForLabels],
[width - paddingForLabels, height - paddingForLabels],
]);
return ({ nodes, links }) =>
sankeyGenerator({
nodes: nodes.map(d => ({ ...d })),
links: links.map(d => ({ ...d })),
});
};
export const labelPosition = ({ x0, x1, y0, y1 }, viewOptions) => {
const { paddingForLabels, labelMargin, nodePadding, width } = viewOptions;
const firstCol = x0 <= paddingForLabels;
const lastCol = x1 >= width - paddingForLabels;
if (firstCol) {
return {
x: 0 + labelMargin,
y: y0,
height: `${y1 - y0}px`,
width: paddingForLabels - 2 * labelMargin,
textAlign: 'right',
};
}
if (lastCol) {
return {
x: width - paddingForLabels + labelMargin,
y: y0,
height: `${y1 - y0}px`,
width: paddingForLabels - 2 * labelMargin,
textAlign: 'left',
};
}
return {
x: (x1 + x0) / 2,
y: y0 - nodePadding,
height: `${nodePadding}px`,
width: 'max-content',
wrapperWidth: paddingForLabels - 2 * labelMargin,
textAlign: x0 < width / 2 ? 'left' : 'right',
};
};
import { sankey, sankeyLeft } from 'd3-sankey';
import { uniqWith, isEqual } from 'lodash';
/*
......@@ -136,34 +135,6 @@ export const parseData = data => {
return { nodes, links };
};
/*
createSankey calls the d3 layout to generate the relationships and positioning
values for the nodes and links in the graph.
*/
export const createSankey = ({
width = 10,
height = 10,
nodeWidth = 10,
nodePadding = 10,
paddingForLabels = 1,
} = {}) => {
const sankeyGenerator = sankey()
.nodeId(({ name }) => name)
.nodeAlign(sankeyLeft)
.nodeWidth(nodeWidth)
.nodePadding(nodePadding)
.extent([
[paddingForLabels, paddingForLabels],
[width - paddingForLabels, height - paddingForLabels],
]);
return ({ nodes, links }) =>
sankeyGenerator({
nodes: nodes.map(d => ({ ...d })),
links: links.map(d => ({ ...d })),
});
};
/*
The number of nodes in the most populous generation drives the height of the graph.
*/
......
......@@ -98,25 +98,27 @@ export default {
:has-issues="reports.length > 0"
class="mr-widget-section grouped-security-reports mr-report"
>
<div slot="body" class="mr-widget-grouped-section report-block">
<template v-for="(report, i) in reports">
<summary-row
:key="`summary-row-${i}`"
:summary="reportText(report)"
:status-icon="getReportIcon(report)"
/>
<issues-list
v-if="shouldRenderIssuesList(report)"
:key="`issues-list-${i}`"
:unresolved-issues="unresolvedIssues(report)"
:new-issues="newIssues(report)"
:resolved-issues="resolvedIssues(report)"
:component="$options.componentNames.TestIssueBody"
class="report-block-group-list"
/>
</template>
<template #body>
<div class="mr-widget-grouped-section report-block">
<template v-for="(report, i) in reports">
<summary-row
:key="`summary-row-${i}`"
:summary="reportText(report)"
:status-icon="getReportIcon(report)"
/>
<issues-list
v-if="shouldRenderIssuesList(report)"
:key="`issues-list-${i}`"
:unresolved-issues="unresolvedIssues(report)"
:new-issues="newIssues(report)"
:resolved-issues="resolvedIssues(report)"
:component="$options.componentNames.TestIssueBody"
class="report-block-group-list"
/>
</template>
<modal :title="modalTitle" :modal-data="modalData" />
</div>
<modal :title="modalTitle" :modal-data="modalData" />
</div>
</template>
</report-section>
</template>
......@@ -70,7 +70,7 @@ $avatar-sizes: (
$identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $identicon-blue, $identicon-teal,
$identicon-orange, $gray-darker;
.avatar-circle {
%avatar-circle {
float: left;
margin-right: $gl-padding;
border-radius: $avatar-radius;
......@@ -84,7 +84,7 @@ $identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $i
}
.avatar {
@extend .avatar-circle;
@extend %avatar-circle;
transition-property: none;
width: 40px;
......@@ -100,10 +100,7 @@ $identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $i
margin-left: 2px;
flex-shrink: 0;
&.s16 {
margin-right: 4px;
}
&.s16,
&.s24 {
margin-right: 4px;
}
......@@ -154,7 +151,7 @@ $identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $i
}
.avatar-container {
@extend .avatar-circle;
@extend %avatar-circle;
overflow: hidden;
display: flex;
......
......@@ -9,8 +9,16 @@
top: 35px;
}
.inactive {
opacity: 0.5;
.design-pin {
transition: opacity 0.5s ease;
&.inactive {
@include gl-opacity-5;
&:hover {
@include gl-opacity-10;
}
}
}
}
......
......@@ -93,7 +93,6 @@
}
.dropdown-menu-toggle,
.avatar-circle,
.header-user-avatar {
@include transition(border-color);
}
......
......@@ -2,6 +2,7 @@
class Projects::ServicesController < Projects::ApplicationController
include ServiceParams
include InternalRedirect
# Authorize
before_action :authorize_admin_project!
......@@ -26,8 +27,8 @@ class Projects::ServicesController < Projects::ApplicationController
respond_to do |format|
format.html do
if saved
redirect_to project_settings_integrations_path(@project),
notice: success_message
target_url = safe_redirect_path(params[:redirect_to]).presence || project_settings_integrations_path(@project)
redirect_to target_url, notice: success_message
else
render 'edit'
end
......
......@@ -12,7 +12,8 @@ module Types
description: 'Name of the link'
field :url, GraphQL::STRING_TYPE, null: true,
description: 'URL of the link'
field :link_type, Types::ReleaseLinkTypeEnum, null: true,
description: 'Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`'
field :external, GraphQL::BOOLEAN_TYPE, null: true, method: :external?,
description: 'Indicates the link points to an external resource'
end
......
# frozen_string_literal: true
module Types
class ReleaseLinkTypeEnum < BaseEnum
graphql_name 'ReleaseLinkType'
description 'Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`'
::Releases::Link.link_types.keys.each do |link_type|
value link_type.upcase, value: link_type, description: "#{link_type.titleize} link type"
end
end
end
......@@ -688,6 +688,10 @@ module Ci
job_artifacts.any?
end
def has_test_reports?
job_artifacts.test_reports.exists?
end
def has_old_trace?
old_trace.present?
end
......
......@@ -11,5 +11,35 @@ module Ci
validates :build, :project, presence: true
validates :data, json_schema: { filename: "build_report_result_data" }
store_accessor :data, :tests
def tests_name
tests.dig("name")
end
def tests_duration
tests.dig("duration")
end
def tests_success
tests.dig("success").to_i
end
def tests_failed
tests.dig("failed").to_i
end
def tests_errored
tests.dig("errored").to_i
end
def tests_skipped
tests.dig("skipped").to_i
end
def tests_total
[tests_success, tests_failed, tests_errored, tests_skipped].sum
end
end
end
# frozen_string_literal: true
module Ci
class BuildReportResultService
def execute(build)
return unless Feature.enabled?(:build_report_summary, build.project)
return unless build.has_test_reports?
build.report_results.create!(
project_id: build.project_id,
data: tests_params(build)
)
end
private
def generate_test_suite_report(build)
build.collect_test_reports!(Gitlab::Ci::Reports::TestReports.new)
end
def tests_params(build)
test_suite = generate_test_suite_report(build)
{
tests: {
name: test_suite.name,
duration: test_suite.total_time,
failed: test_suite.failed_count,
errored: test_suite.error_count,
skipped: test_suite.skipped_count,
success: test_suite.success_count
}
}
end
end
end
......@@ -4,8 +4,6 @@
# Example:
# ```
# class DummyService
# prepend Measurable
#
# def execute
# # ...
# end
......
......@@ -3,9 +3,9 @@
"type": "object",
"properties": {
"coverage": { "type": "float" },
"junit": {
"tests": {
"type": "object",
"items": { "$ref": "./build_report_result_data_junit.json" }
"items": { "$ref": "./build_report_result_data_tests.json" }
}
},
"additionalProperties": false
......
{
"description": "Build report result data junit",
"description": "Build report result data tests",
"type": "object",
"properties": {
"name": { "type": "string" },
......
......@@ -13,6 +13,7 @@
= form_for(@service, as: :service, url: scoped_integration_path(@service), method: :put, html: { class: 'gl-show-field-errors integration-settings-form js-integration-settings-form', data: { 'can-test' => @service.can_test?, 'test-url' => test_project_service_path(@project, @service) } }) do |form|
= render 'shared/service_settings', form: form, service: @service
.footer-block.row-content-block
%input{ id: 'services_redirect_to', type: 'hidden', name: 'redirect_to', value: request.referrer }
= service_save_button
&nbsp;
= link_to _('Cancel'), project_settings_integrations_path(@project), class: 'btn btn-cancel'
......
......@@ -803,6 +803,14 @@
:weight: 1
:idempotent:
:tags: []
- :name: pipeline_background:ci_build_report_result
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
:weight: 1
:idempotent: true
:tags: []
- :name: pipeline_background:ci_build_trace_chunk_flush
:feature_category: :continuous_integration
:has_external_dependencies:
......
......@@ -28,6 +28,7 @@ class BuildFinishedWorker # rubocop:disable Scalability/IdempotentWorker
# We execute these in sync to reduce IO.
BuildTraceSectionsWorker.new.perform(build.id)
BuildCoverageWorker.new.perform(build.id)
Ci::BuildReportResultWorker.new.perform(build.id)
# We execute these async as these are independent operations.
BuildHooksWorker.perform_async(build.id)
......
# frozen_string_literal: true
module Ci
class BuildReportResultWorker
include ApplicationWorker
include PipelineBackgroundQueue
idempotent!
def perform(build_id)
Ci::Build.find_by_id(build_id).try do |build|
Ci::BuildReportResultService.new.execute(build)
end
end
end
end
......@@ -21,10 +21,6 @@ module Secpick
@options = self.class.options
end
def ee?
File.exist?(File.expand_path('../ee/app/models/license.rb', __dir__))
end
def dry_run?
@options[:try] == true
end
......@@ -40,9 +36,7 @@ module Secpick
end
def stable_branch
"#{@options[:version]}-#{STABLE_SUFFIX}".tap do |name|
name << "-ee" if ee?
end.freeze
"#{@options[:version]}-#{STABLE_SUFFIX}-ee".freeze
end
def git_commands
......@@ -64,11 +58,7 @@ module Secpick
end
def new_mr_url
if ee?
SECURITY_MR_URL
else
SECURITY_MR_URL.sub('/gitlab/', '/gitlab-foss/')
end
SECURITY_MR_URL
end
def create!
......
---
title: Add api.js methods to update issues and merge requests
merge_request: 32893
author:
type: added
---
title: Expose `release_links.type` via API
merge_request: 33154
author:
type: changed
---
title: Add `link_type` to `ReleaseLink` GraphQL type
merge_request: 33386
author:
type: added
---
title: Add opacity transition to active design discussion pins
merge_request: 33493
author:
type: other
---
title: Update deprecated slot syntax in app/assets/javascripts/reports/components/grouped_test_reports_app.vue
merge_request: 31975
author: Gilang Gumilar
type: other
......@@ -9648,6 +9648,11 @@ type ReleaseLink {
"""
id: ID!
"""
Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`
"""
linkType: ReleaseLinkType
"""
Name of the link
"""
......@@ -9694,6 +9699,31 @@ type ReleaseLinkEdge {
node: ReleaseLink
}
"""
Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`
"""
enum ReleaseLinkType {
"""
Image link type
"""
IMAGE
"""
Other link type
"""
OTHER
"""
Package link type
"""
PACKAGE
"""
Runbook link type
"""
RUNBOOK
}
type ReleaseSource {
"""
Format of the source
......
......@@ -28194,6 +28194,20 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "linkType",
"description": "Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`",
"args": [
],
"type": {
"kind": "ENUM",
"name": "ReleaseLinkType",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "name",
"description": "Name of the link",
......@@ -28342,6 +28356,41 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "ReleaseLinkType",
"description": "Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": [
{
"name": "OTHER",
"description": "Other link type",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "RUNBOOK",
"description": "Runbook link type",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "PACKAGE",
"description": "Package link type",
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "IMAGE",
"description": "Image link type",
"isDeprecated": false,
"deprecationReason": null
}
],
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "ReleaseSource",
......@@ -1342,6 +1342,7 @@ Information about pagination in a connection.
| --- | ---- | ---------- |
| `external` | Boolean | Indicates the link points to an external resource |
| `id` | ID! | ID of the link |
| `linkType` | ReleaseLinkType | Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other` |
| `name` | String | Name of the link |
| `url` | String | URL of the link |
......
......@@ -42,6 +42,7 @@ GitLab provides built-in tools to help improve performance and availability:
- [Request Profiling](../administration/monitoring/performance/request_profiling.md).
- [QueryRecoder](query_recorder.md) for preventing `N+1` regressions.
- [Chaos endpoints](chaos_endpoints.md) for testing failure scenarios. Intended mainly for testing availability.
- [Service measurement](service_measurement.md) for measuring and logging service execution.
GitLab team members can use [GitLab.com's performance monitoring systems](https://about.gitlab.com/handbook/engineering/monitoring/) located at
<https://dashboards.gitlab.net>, this requires you to log in using your
......
# GitLab Developers Guide to service measurement
You can enable service measurement in order to debug any slow service's execution time, number of SQL calls, garbage collection stats, memory usage, etc.
## Measuring module
The measuring module is a tool that allows to measure a service's execution, and log:
- Service class name
- Execution time
- Number of sql calls
- Detailed gc stats and diffs
- RSS memory usage
- Server worker ID
The measuring module will log these measurements into a structured log called [`service_measurement.log`](../administration/logs.md#service_measurementlog),
as a single entry for each service execution.
NOTE: **Note:**
For GitLab.com, `service_measurement.log` is ingested in Elasticsearch and Kibana as part of our monitoring solution.
## How to use it
The measuring module allows you to easily measure and log execution of any service,
by just prepending `Measurable` in any Service class, on the last line of the file that the class resides in.
For example, to prepend a module into the `DummyService` class, you would use the following approach:
```ruby
class DummyService
def execute
# ...
end
end
DummyService.prepend(Measurable)
```
In case when you are prepending a module from the `EE` namespace with EE features, you need to prepend Measurable after prepending the `EE` module.
This way, `Measurable` will be at the bottom of the ancestor chain, in order to measure execution of `EE` features as well:
```ruby
class DummyService
def execute
# ...
end
end
DummyService.prepend_if_ee('EE::DummyService')
DummyService.prepend(Measurable)
```
### Log additional attributes
In case you need to log some additional attributes, it is possible to define `extra_attributes_for_measurement` in the service class:
```ruby
def extra_attributes_for_measurement
{
project_path: @project.full_path,
user: current_user.name
}
end
```
NOTE: **Note:**
Once the measurement module is injected in the service, it will be behind generic feature flag.
In order to actually use it, you need to enable measuring for the desired service by enabling the feature flag.
### Enabling measurement using feature flags
In the following example, the `:gitlab_service_measuring_projects_import_service`
[feature flag](feature_flags/development.md#enabling-a-feature-flag-in-development) is used to enable the measuring feature
for `Projects::ImportService`.
From chatops:
```shell
/chatops run feature set gitlab_service_measuring_projects_import_service true
```
......@@ -9,6 +9,7 @@ module API
expose :url
expose :direct_asset_url
expose :external?, as: :external
expose :link_type
def direct_asset_url
return object.url unless object.filepath
......
......@@ -40,6 +40,7 @@ module API
requires :name, type: String, desc: 'The name of the link'
requires :url, type: String, desc: 'The URL of the link'
optional :filepath, type: String, desc: 'The filepath of the link'
optional :link_type, type: String, desc: 'The link type'
end
post 'links' do
authorize! :create_release, release
......@@ -75,6 +76,7 @@ module API
optional :name, type: String, desc: 'The name of the link'
optional :url, type: String, desc: 'The URL of the link'
optional :filepath, type: String, desc: 'The filepath of the link'
optional :link_type, type: String, desc: 'The link type'
at_least_one_of :name, :url
end
put do
......
......@@ -134,24 +134,50 @@ describe Projects::ServicesController do
describe 'PUT #update' do
describe 'as HTML' do
let(:service_params) { { active: true } }
let(:params) { project_params(service: service_params) }
let(:message) { 'Jira activated.' }
let(:redirect_url) { project_settings_integrations_path(project) }
before do
put :update, params: project_params(service: service_params)
put :update, params: params
end
shared_examples 'service update' do
it 'redirects to the correct url with a flash message' do
expect(response).to redirect_to(redirect_url)
expect(flash[:notice]).to eq(message)
end
end
context 'when param `active` is set to true' do
it 'activates the service and redirects to integrations paths' do
expect(response).to redirect_to(project_settings_integrations_path(project))
expect(flash[:notice]).to eq 'Jira activated.'
let(:params) { project_params(service: service_params, redirect_to: redirect) }
context 'when redirect_to param is present' do
let(:redirect) { '/redirect_here' }
let(:redirect_url) { redirect }
it_behaves_like 'service update'
end
context 'when redirect_to is an external domain' do
let(:redirect) { 'http://examle.com' }
it_behaves_like 'service update'
end
context 'when redirect_to param is an empty string' do
let(:redirect) { '' }
it_behaves_like 'service update'
end
end
context 'when param `active` is set to false' do
let(:service_params) { { active: false } }
let(:message) { 'Jira settings saved, but not activated.' }
it 'does not activate the service but saves the settings' do
expect(flash[:notice]).to eq 'Jira settings saved, but not activated.'
end
it_behaves_like 'service update'
end
end
......
......@@ -6,7 +6,7 @@ FactoryBot.define do
project factory: :project
data do
{
junit: {
tests: {
name: "rspec",
duration: 0.42,
failed: 0,
......@@ -20,7 +20,7 @@ FactoryBot.define do
trait :with_junit_success do
data do
{
junit: {
tests: {
name: "rspec",
duration: 0.42,
failed: 0,
......
......@@ -6,5 +6,6 @@ FactoryBot.define do
sequence(:name) { |n| "release-18.#{n}.dmg" }
sequence(:url) { |n| "https://example.com/scrambled-url/app-#{n}.zip" }
sequence(:filepath) { |n| "/binaries/awesome-app-#{n}" }
link_type { 'other' }
end
end
......@@ -7,7 +7,8 @@
"filepath": { "type": "string" },
"url": { "type": "string" },
"direct_asset_url": { "type": "string" },
"external": { "type": "boolean" }
"external": { "type": "boolean" },
"link_type": { "type": "string" }
},
"additionalProperties": false
}
......@@ -691,4 +691,38 @@ describe('Api', () => {
});
});
});
describe('updateIssue', () => {
it('update an issue with the given payload', done => {
const projectId = 8;
const issue = 1;
const expectedArray = [1, 2, 3];
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/issues/${issue}`;
mock.onPut(expectedUrl).reply(200, { assigneeIds: expectedArray });
Api.updateIssue(projectId, issue, { assigneeIds: expectedArray })
.then(({ data }) => {
expect(data.assigneeIds).toEqual(expectedArray);
done();
})
.catch(done.fail);
});
});
describe('updateMergeRequest', () => {
it('update an issue with the given payload', done => {
const projectId = 8;
const mergeRequest = 1;
const expectedArray = [1, 2, 3];
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/merge_requests/${mergeRequest}`;
mock.onPut(expectedUrl).reply(200, { assigneeIds: expectedArray });
Api.updateMergeRequest(projectId, mergeRequest, { assigneeIds: expectedArray })
.then(({ data }) => {
expect(data.assigneeIds).toEqual(expectedArray);
done();
})
.catch(done.fail);
});
});
});
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Design discussions component should match the snapshot of note when repositioning 1`] = `
exports[`Design note pin component should match the snapshot of note when repositioning 1`] = `
<button
aria-label="Comment form position"
class="position-absolute btn-transparent comment-indicator"
class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center btn-transparent comment-indicator"
style="left: 10px; top: 10px; cursor: move;"
type="button"
>
......@@ -14,10 +14,10 @@ exports[`Design discussions component should match the snapshot of note when rep
</button>
`;
exports[`Design discussions component should match the snapshot of note with index 1`] = `
exports[`Design note pin component should match the snapshot of note with index 1`] = `
<button
aria-label="Comment '1' position"
class="position-absolute js-image-badge badge badge-pill"
class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center js-image-badge badge badge-pill"
style="left: 10px; top: 10px;"
type="button"
>
......@@ -27,10 +27,10 @@ exports[`Design discussions component should match the snapshot of note with ind
</button>
`;
exports[`Design discussions component should match the snapshot of note without index 1`] = `
exports[`Design note pin component should match the snapshot of note without index 1`] = `
<button
aria-label="Comment form position"
class="position-absolute btn-transparent comment-indicator"
class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center btn-transparent comment-indicator"
style="left: 10px; top: 10px;"
type="button"
>
......
import { shallowMount } from '@vue/test-utils';
import DesignNotePin from '~/design_management/components/design_note_pin.vue';
describe('Design discussions component', () => {
describe('Design note pin component', () => {
let wrapper;
function createComponent(propsData = {}) {
......
......@@ -10,12 +10,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
</linearGradient>
<clipPath id=\\"dag-clip63\\">
<path d=\\"
M100, 129
V158
H377.3333333333333
V100
H100
Z\\"></path>
M100, 129
V158
H377.3333333333333
V100
H100
Z
\\"></path>
</clipPath>
<path d=\\"M108,129L190,129L190,129L369.3333333333333,129\\" stroke=\\"url(#dag-grad53)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip63)\\"></path>
</g>
......@@ -26,12 +27,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
</linearGradient>
<clipPath id=\\"dag-clip64\\">
<path d=\\"
M361.3333333333333, 129.0000000000002
V158.0000000000002
H638.6666666666666
V100
H361.3333333333333
Z\\"></path>
M361.3333333333333, 129.0000000000002
V158.0000000000002
H638.6666666666666
V100
H361.3333333333333
Z
\\"></path>
</clipPath>
<path d=\\"M369.3333333333333,129L509.3333333333333,129L509.3333333333333,129.0000000000002L630.6666666666666,129.0000000000002\\" stroke=\\"url(#dag-grad54)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip64)\\"></path>
</g>
......@@ -42,12 +44,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
</linearGradient>
<clipPath id=\\"dag-clip65\\">
<path d=\\"
M100, 187.0000000000002
V241.00000000000003
H638.6666666666666
V158.0000000000002
H100
Z\\"></path>
M100, 187.0000000000002
V241.00000000000003
H638.6666666666666
V158.0000000000002
H100
Z
\\"></path>
</clipPath>
<path d=\\"M108,212.00000000000003L306,212.00000000000003L306,187.0000000000002L630.6666666666666,187.0000000000002\\" stroke=\\"url(#dag-grad55)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip65)\\"></path>
</g>
......@@ -58,12 +61,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
</linearGradient>
<clipPath id=\\"dag-clip66\\">
<path d=\\"
M100, 269.9999999999998
V324
H377.3333333333333
V240.99999999999977
H100
Z\\"></path>
M100, 269.9999999999998
V324
H377.3333333333333
V240.99999999999977
H100
Z
\\"></path>
</clipPath>
<path d=\\"M108,295L338.93333333333334,295L338.93333333333334,269.9999999999998L369.3333333333333,269.9999999999998\\" stroke=\\"url(#dag-grad56)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip66)\\"></path>
</g>
......@@ -74,12 +78,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
</linearGradient>
<clipPath id=\\"dag-clip67\\">
<path d=\\"
M100, 352.99999999999994
V407.00000000000006
H377.3333333333333
V323.99999999999994
H100
Z\\"></path>
M100, 352.99999999999994
V407.00000000000006
H377.3333333333333
V323.99999999999994
H100
Z
\\"></path>
</clipPath>
<path d=\\"M108,378.00000000000006L144.66666666666669,378.00000000000006L144.66666666666669,352.99999999999994L369.3333333333333,352.99999999999994\\" stroke=\\"url(#dag-grad57)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip67)\\"></path>
</g>
......@@ -90,12 +95,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
</linearGradient>
<clipPath id=\\"dag-clip68\\">
<path d=\\"
M361.3333333333333, 270.0000000000001
V299.0000000000001
H638.6666666666666
V240.99999999999977
H361.3333333333333
Z\\"></path>
M361.3333333333333, 270.0000000000001
V299.0000000000001
H638.6666666666666
V240.99999999999977
H361.3333333333333
Z
\\"></path>
</clipPath>
<path d=\\"M369.3333333333333,269.9999999999998L464,269.9999999999998L464,270.0000000000001L630.6666666666666,270.0000000000001\\" stroke=\\"url(#dag-grad58)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip68)\\"></path>
</g>
......@@ -106,12 +112,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
</linearGradient>
<clipPath id=\\"dag-clip69\\">
<path d=\\"
M361.3333333333333, 328.0000000000001
V381.99999999999994
H638.6666666666666
V299.0000000000001
H361.3333333333333
Z\\"></path>
M361.3333333333333, 328.0000000000001
V381.99999999999994
H638.6666666666666
V299.0000000000001
H361.3333333333333
Z
\\"></path>
</clipPath>
<path d=\\"M369.3333333333333,352.99999999999994L522,352.99999999999994L522,328.0000000000001L630.6666666666666,328.0000000000001\\" stroke=\\"url(#dag-grad59)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip69)\\"></path>
</g>
......@@ -122,12 +129,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
</linearGradient>
<clipPath id=\\"dag-clip70\\">
<path d=\\"
M361.3333333333333, 411
V440
H638.6666666666666
V381.99999999999994
H361.3333333333333
Z\\"></path>
M361.3333333333333, 411
V440
H638.6666666666666
V381.99999999999994
H361.3333333333333
Z
\\"></path>
</clipPath>
<path d=\\"M369.3333333333333,410.99999999999994L580,410.99999999999994L580,411L630.6666666666666,411\\" stroke=\\"url(#dag-grad60)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip70)\\"></path>
</g>
......@@ -138,12 +146,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
</linearGradient>
<clipPath id=\\"dag-clip71\\">
<path d=\\"
M622.6666666666666, 270.1890725105691
V299.1890725105691
H900
V241.0000000000001
H622.6666666666666
Z\\"></path>
M622.6666666666666, 270.1890725105691
V299.1890725105691
H900
V241.0000000000001
H622.6666666666666
Z
\\"></path>
</clipPath>
<path d=\\"M630.6666666666666,270.0000000000001L861.6,270.0000000000001L861.6,270.1890725105691L892,270.1890725105691\\" stroke=\\"url(#dag-grad61)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip71)\\"></path>
</g>
......@@ -154,12 +163,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
</linearGradient>
<clipPath id=\\"dag-clip72\\">
<path d=\\"
M622.6666666666666, 411
V440
H900
V382
H622.6666666666666
Z\\"></path>
M622.6666666666666, 411
V440
H900
V382
H622.6666666666666
Z
\\"></path>
</clipPath>
<path d=\\"M630.6666666666666,411L679.9999999999999,411L679.9999999999999,411L892,411\\" stroke=\\"url(#dag-grad62)\\" style=\\"stroke-linejoin: round;\\" stroke-width=\\"56\\" clip-path=\\"url(#dag-clip72)\\"></path>
</g>
......
import { mount } from '@vue/test-utils';
import DagGraph from '~/pipelines/components/dag/dag_graph.vue';
import { createSankey, removeOrphanNodes } from '~/pipelines/components/dag/utils';
import { createSankey } from '~/pipelines/components/dag/drawing_utils';
import { removeOrphanNodes } from '~/pipelines/components/dag/parsing_utils';
import { parsedData } from './mock_data';
describe('The DAG graph', () => {
......
import { createSankey } from '~/pipelines/components/dag/drawing_utils';
import { parseData } from '~/pipelines/components/dag/parsing_utils';
import { mockBaseData } from './mock_data';
describe('DAG visualization drawing utilities', () => {
const parsed = parseData(mockBaseData.stages);
const layoutSettings = {
width: 200,
height: 200,
nodeWidth: 10,
nodePadding: 20,
paddingForLabels: 100,
};
const sankeyLayout = createSankey(layoutSettings)(parsed);
describe('createSankey', () => {
it('returns a nodes data structure with expected d3-added properties', () => {
const exampleNode = sankeyLayout.nodes[0];
expect(exampleNode).toHaveProperty('sourceLinks');
expect(exampleNode).toHaveProperty('targetLinks');
expect(exampleNode).toHaveProperty('depth');
expect(exampleNode).toHaveProperty('layer');
expect(exampleNode).toHaveProperty('x0');
expect(exampleNode).toHaveProperty('x1');
expect(exampleNode).toHaveProperty('y0');
expect(exampleNode).toHaveProperty('y1');
});
it('returns a links data structure with expected d3-added properties', () => {
const exampleLink = sankeyLayout.links[0];
expect(exampleLink).toHaveProperty('source');
expect(exampleLink).toHaveProperty('target');
expect(exampleLink).toHaveProperty('width');
expect(exampleLink).toHaveProperty('y0');
expect(exampleLink).toHaveProperty('y1');
});
describe('data structure integrity', () => {
const newObject = { name: 'bad-actor' };
beforeEach(() => {
sankeyLayout.nodes.unshift(newObject);
});
it('sankey does not propagate changes back to the original', () => {
expect(sankeyLayout.nodes[0]).toBe(newObject);
expect(parsed.nodes[0]).not.toBe(newObject);
});
afterEach(() => {
sankeyLayout.nodes.shift();
});
});
});
});
......@@ -3,11 +3,11 @@ import {
makeLinksFromNodes,
filterByAncestors,
parseData,
createSankey,
removeOrphanNodes,
getMaxNodes,
} from '~/pipelines/components/dag/utils';
} from '~/pipelines/components/dag/parsing_utils';
import { createSankey } from '~/pipelines/components/dag/drawing_utils';
import { mockBaseData } from './mock_data';
describe('DAG visualization parsing utilities', () => {
......@@ -105,44 +105,6 @@ describe('DAG visualization parsing utilities', () => {
});
});
describe('createSankey', () => {
it('returns a nodes data structure with expected d3-added properties', () => {
expect(sankeyLayout.nodes[0]).toHaveProperty('sourceLinks');
expect(sankeyLayout.nodes[0]).toHaveProperty('targetLinks');
expect(sankeyLayout.nodes[0]).toHaveProperty('depth');
expect(sankeyLayout.nodes[0]).toHaveProperty('layer');
expect(sankeyLayout.nodes[0]).toHaveProperty('x0');
expect(sankeyLayout.nodes[0]).toHaveProperty('x1');
expect(sankeyLayout.nodes[0]).toHaveProperty('y0');
expect(sankeyLayout.nodes[0]).toHaveProperty('y1');
});
it('returns a links data structure with expected d3-added properties', () => {
expect(sankeyLayout.links[0]).toHaveProperty('source');
expect(sankeyLayout.links[0]).toHaveProperty('target');
expect(sankeyLayout.links[0]).toHaveProperty('width');
expect(sankeyLayout.links[0]).toHaveProperty('y0');
expect(sankeyLayout.links[0]).toHaveProperty('y1');
});
describe('data structure integrity', () => {
const newObject = { name: 'bad-actor' };
beforeEach(() => {
sankeyLayout.nodes.unshift(newObject);
});
it('sankey does not propagate changes back to the original', () => {
expect(sankeyLayout.nodes[0]).toBe(newObject);
expect(parsed.nodes[0]).not.toBe(newObject);
});
afterEach(() => {
sankeyLayout.nodes.shift();
});
});
});
describe('removeOrphanNodes', () => {
it('removes sankey nodes that have no needs and are not needed', () => {
const cleanedNodes = removeOrphanNodes(sankeyLayout.nodes);
......
......@@ -7,7 +7,7 @@ describe GitlabSchema.types['ReleaseLink'] do
it 'has the expected fields' do
expected_fields = %w[
id name url external
id name url external link_type
]
expect(described_class).to include_graphql_fields(*expected_fields)
......
......@@ -29,4 +29,46 @@ describe Ci::BuildReportResult do
end
end
end
describe '#tests_name' do
it 'returns the suite name' do
expect(build_report_result.tests_name).to eq("rspec")
end
end
describe '#tests_duration' do
it 'returns the suite duration' do
expect(build_report_result.tests_duration).to eq(0.42)
end
end
describe '#tests_success' do
it 'returns the success count' do
expect(build_report_result.tests_success).to eq(2)
end
end
describe '#tests_failed' do
it 'returns the failed count' do
expect(build_report_result.tests_failed).to eq(0)
end
end
describe '#tests_errored' do
it 'returns the errored count' do
expect(build_report_result.tests_errored).to eq(0)
end
end
describe '#tests_skipped' do
it 'returns the skipped count' do
expect(build_report_result.tests_skipped).to eq(0)
end
end
describe '#tests_total' do
it 'returns the total count' do
expect(build_report_result.tests_total).to eq(2)
end
end
end
......@@ -875,6 +875,22 @@ describe Ci::Build do
end
end
describe '#has_test_reports?' do
subject { build.has_test_reports? }
context 'when build has a test report' do
let(:build) { create(:ci_build, :test_reports) }
it { is_expected.to be_truthy }
end
context 'when build does not have a test report' do
let(:build) { create(:ci_build) }
it { is_expected.to be_falsey }
end
end
describe '#has_old_trace?' do
subject { build.has_old_trace? }
......
# frozen_string_literal: true
require 'spec_helper'
describe Ci::BuildReportResultService do
describe "#execute" do
subject(:build_report_result) { described_class.new.execute(build) }
context 'when build is finished' do
let(:build) { create(:ci_build, :success, :test_reports) }
it 'creates a build report result entry', :aggregate_failures do
expect(build_report_result.tests_name).to eq("test")
expect(build_report_result.tests_success).to eq(2)
expect(build_report_result.tests_failed).to eq(2)
expect(build_report_result.tests_errored).to eq(0)
expect(build_report_result.tests_skipped).to eq(0)
expect(build_report_result.tests_duration).to eq(0.010284)
expect(Ci::BuildReportResult.count).to eq(1)
end
context 'when feature is disable' do
it 'does not persist the data' do
stub_feature_flags(build_report_summary: false)
subject
expect(Ci::BuildReportResult.count).to eq(0)
end
end
context 'when data has already been persisted' do
it 'raises an error and do not persist the same data twice' do
expect { 2.times { described_class.new.execute(build) } }.to raise_error(ActiveRecord::RecordNotUnique)
expect(Ci::BuildReportResult.count).to eq(1)
end
end
end
context 'when build is running and test report does not exist' do
let(:build) { create(:ci_build, :running) }
it 'does not persist data' do
subject
expect(Ci::BuildReportResult.count).to eq(0)
end
end
end
end
......@@ -15,7 +15,8 @@ describe 'projects/services/_form' do
allow(view).to receive_messages(current_user: user,
can?: true,
current_application_settings: Gitlab::CurrentSettings.current_application_settings)
current_application_settings: Gitlab::CurrentSettings.current_application_settings,
request: double(referrer: '/services'))
end
context 'commit_events and merge_request_events' do
......@@ -30,6 +31,7 @@ describe 'projects/services/_form' do
expect(rendered).to have_content('Event will be triggered when a commit is created/updated')
expect(rendered).to have_content('Event will be triggered when a merge request is created/updated/merged')
expect(rendered).to have_css("input[name='redirect_to'][value='/services']", count: 1, visible: false)
end
end
end
......@@ -3,7 +3,11 @@
require 'spec_helper'
describe BuildFinishedWorker do
subject { described_class.new.perform(build.id) }
describe '#perform' do
let(:build) { create(:ci_build, :success, pipeline: create(:ci_pipeline)) }
context 'when build exists' do
let!(:build) { create(:ci_build) }
......@@ -18,8 +22,10 @@ describe BuildFinishedWorker do
expect(BuildHooksWorker).to receive(:perform_async)
expect(ArchiveTraceWorker).to receive(:perform_async)
expect(ExpirePipelineCacheWorker).to receive(:perform_async)
expect(ChatNotificationWorker).not_to receive(:perform_async)
expect(Ci::BuildReportResultWorker).not_to receive(:perform)
described_class.new.perform(build.id)
subject
end
end
......@@ -30,23 +36,26 @@ describe BuildFinishedWorker do
end
end
it 'schedules a ChatNotification job for a chat build' do
build = create(:ci_build, :success, pipeline: create(:ci_pipeline, source: :chat))
context 'when build has a chat' do
let(:build) { create(:ci_build, :success, pipeline: create(:ci_pipeline, source: :chat)) }
expect(ChatNotificationWorker)
.to receive(:perform_async)
.with(build.id)
it 'schedules a ChatNotification job' do
expect(ChatNotificationWorker).to receive(:perform_async).with(build.id)
described_class.new.perform(build.id)
subject
end
end
it 'does not schedule a ChatNotification job for a regular build' do
build = create(:ci_build, :success, pipeline: create(:ci_pipeline))
context 'when build has a test report' do
let(:build) { create(:ci_build, :test_reports) }
expect(ChatNotificationWorker)
.not_to receive(:perform_async)
it 'schedules a BuildReportResult job' do
expect_next_instance_of(Ci::BuildReportResultWorker) do |worker|
expect(worker).to receive(:perform).with(build.id)
end
described_class.new.perform(build.id)
subject
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Ci::BuildReportResultWorker do
subject { described_class.new.perform(build_id) }
context 'when build exists' do
let(:build) { create(:ci_build) }
let(:build_id) { build.id }
it 'calls build report result service' do
expect_next_instance_of(Ci::BuildReportResultService) do |build_report_result_service|
expect(build_report_result_service).to receive(:execute)
end
subject
end
end
context 'when build does not exist' do
let(:build_id) { -1 }
it 'does not call build report result service' do
expect(Ci::BuildReportResultService).not_to receive(:execute)
subject
end
end
end
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册