Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-06-02 21:08:00 +00:00
parent f3e7bc8060
commit 2164573e45
54 changed files with 878 additions and 321 deletions

View file

@ -51,6 +51,7 @@ const Api = {
pipelinesPath: '/api/:version/projects/:id/pipelines/', pipelinesPath: '/api/:version/projects/:id/pipelines/',
environmentsPath: '/api/:version/projects/:id/environments', environmentsPath: '/api/:version/projects/:id/environments',
rawFilePath: '/api/:version/projects/:id/repository/files/:path/raw', rawFilePath: '/api/:version/projects/:id/repository/files/:path/raw',
issuePath: '/api/:version/projects/:id/issues/:issue_iid',
group(groupId, callback) { group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId); const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
@ -540,6 +541,22 @@ const Api = {
return axios.get(url, { params }); 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) { buildUrl(url) {
return joinPaths(gon.relative_url_root || '', url.replace(':version', gon.api_version)); return joinPaths(gon.relative_url_root || '', url.replace(':version', gon.api_version));
}, },

View file

@ -47,7 +47,7 @@ export default {
'btn-transparent comment-indicator': isNewNote, 'btn-transparent comment-indicator': isNewNote,
'js-image-badge badge badge-pill': !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" type="button"
@mousedown="$emit('mousedown', $event)" @mousedown="$emit('mousedown', $event)"
@mouseup="$emit('mouseup', $event)" @mouseup="$emit('mouseup', $event)"

View file

@ -4,7 +4,7 @@ import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import DagGraph from './dag_graph.vue'; import DagGraph from './dag_graph.vue';
import { DEFAULT, PARSE_FAILURE, LOAD_FAILURE, UNSUPPORTED_DATA } from './constants'; import { DEFAULT, PARSE_FAILURE, LOAD_FAILURE, UNSUPPORTED_DATA } from './constants';
import { parseData } from './utils'; import { parseData } from './parsing_utils';
export default { export default {
// eslint-disable-next-line @gitlab/require-i18n-strings // eslint-disable-next-line @gitlab/require-i18n-strings

View file

@ -3,7 +3,8 @@ import * as d3 from 'd3';
import { uniqueId } from 'lodash'; import { uniqueId } from 'lodash';
import { PARSE_FAILURE } from './constants'; 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 { export default {
viewOptions: { viewOptions: {
@ -78,7 +79,7 @@ export default {
return ( return (
link link
.append('path') .append('path')
.attr('d', this.createLinkPath) .attr('d', (d, i) => createLinkPath(d, i, this.$options.viewOptions.nodeWidth))
.attr('stroke', ({ gradId }) => `url(#${gradId})`) .attr('stroke', ({ gradId }) => `url(#${gradId})`)
.style('stroke-linejoin', 'round') .style('stroke-linejoin', 'round')
// minus two to account for the rounded nodes // minus two to account for the rounded nodes
@ -89,7 +90,10 @@ export default {
appendLabelAsForeignObject(d, i, n) { appendLabelAsForeignObject(d, i, n) {
const currentNode = n[i]; 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 = [ const labelClasses = [
'gl-display-flex', 'gl-display-flex',
@ -128,44 +132,13 @@ export default {
}, },
createClip(link) { 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 return link
.append('clipPath') .append('clipPath')
.attr('id', d => { .attr('id', d => {
return this.createAndAssignId(d, 'clipId', 'dag-clip'); return this.createAndAssignId(d, 'clipId', 'dag-clip');
}) })
.append('path') .append('path')
.attr('d', clip); .attr('d', calculateClip);
}, },
createGradient(link) { createGradient(link) {
@ -189,44 +162,6 @@ export default {
.attr('stop-color', ({ target }) => this.color(target)); .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) { createLinks(svg, linksData) {
const link = this.generateLinks(svg, linksData); const link = this.generateLinks(svg, linksData);
this.createGradient(link); this.createGradient(link);
@ -322,42 +257,6 @@ export default {
return ({ name }) => colorFn(name); 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) { transformData(parsed) {
const baseLayout = createSankey()(parsed); const baseLayout = createSankey()(parsed);
const cleanedNodes = removeOrphanNodes(baseLayout.nodes); const cleanedNodes = removeOrphanNodes(baseLayout.nodes);

View file

@ -0,0 +1,134 @@
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',
};
};

View file

@ -1,4 +1,3 @@
import { sankey, sankeyLeft } from 'd3-sankey';
import { uniqWith, isEqual } from 'lodash'; import { uniqWith, isEqual } from 'lodash';
/* /*
@ -136,34 +135,6 @@ export const parseData = data => {
return { nodes, links }; 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. The number of nodes in the most populous generation drives the height of the graph.
*/ */

View file

@ -98,25 +98,27 @@ export default {
:has-issues="reports.length > 0" :has-issues="reports.length > 0"
class="mr-widget-section grouped-security-reports mr-report" class="mr-widget-section grouped-security-reports mr-report"
> >
<div slot="body" class="mr-widget-grouped-section report-block"> <template #body>
<template v-for="(report, i) in reports"> <div class="mr-widget-grouped-section report-block">
<summary-row <template v-for="(report, i) in reports">
:key="`summary-row-${i}`" <summary-row
:summary="reportText(report)" :key="`summary-row-${i}`"
:status-icon="getReportIcon(report)" :summary="reportText(report)"
/> :status-icon="getReportIcon(report)"
<issues-list />
v-if="shouldRenderIssuesList(report)" <issues-list
:key="`issues-list-${i}`" v-if="shouldRenderIssuesList(report)"
:unresolved-issues="unresolvedIssues(report)" :key="`issues-list-${i}`"
:new-issues="newIssues(report)" :unresolved-issues="unresolvedIssues(report)"
:resolved-issues="resolvedIssues(report)" :new-issues="newIssues(report)"
:component="$options.componentNames.TestIssueBody" :resolved-issues="resolvedIssues(report)"
class="report-block-group-list" :component="$options.componentNames.TestIssueBody"
/> class="report-block-group-list"
</template> />
</template>
<modal :title="modalTitle" :modal-data="modalData" /> <modal :title="modalTitle" :modal-data="modalData" />
</div> </div>
</template>
</report-section> </report-section>
</template> </template>

View file

@ -70,7 +70,7 @@ $avatar-sizes: (
$identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $identicon-blue, $identicon-teal, $identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $identicon-blue, $identicon-teal,
$identicon-orange, $gray-darker; $identicon-orange, $gray-darker;
.avatar-circle { %avatar-circle {
float: left; float: left;
margin-right: $gl-padding; margin-right: $gl-padding;
border-radius: $avatar-radius; border-radius: $avatar-radius;
@ -84,7 +84,7 @@ $identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $i
} }
.avatar { .avatar {
@extend .avatar-circle; @extend %avatar-circle;
transition-property: none; transition-property: none;
width: 40px; width: 40px;
@ -100,10 +100,7 @@ $identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $i
margin-left: 2px; margin-left: 2px;
flex-shrink: 0; flex-shrink: 0;
&.s16 { &.s16,
margin-right: 4px;
}
&.s24 { &.s24 {
margin-right: 4px; margin-right: 4px;
} }
@ -154,7 +151,7 @@ $identicon-backgrounds: $identicon-red, $identicon-purple, $identicon-indigo, $i
} }
.avatar-container { .avatar-container {
@extend .avatar-circle; @extend %avatar-circle;
overflow: hidden; overflow: hidden;
display: flex; display: flex;

View file

@ -9,8 +9,16 @@
top: 35px; top: 35px;
} }
.inactive { .design-pin {
opacity: 0.5; transition: opacity 0.5s ease;
&.inactive {
@include gl-opacity-5;
&:hover {
@include gl-opacity-10;
}
}
} }
} }

View file

@ -93,7 +93,6 @@
} }
.dropdown-menu-toggle, .dropdown-menu-toggle,
.avatar-circle,
.header-user-avatar { .header-user-avatar {
@include transition(border-color); @include transition(border-color);
} }

View file

@ -2,6 +2,7 @@
class Projects::ServicesController < Projects::ApplicationController class Projects::ServicesController < Projects::ApplicationController
include ServiceParams include ServiceParams
include InternalRedirect
# Authorize # Authorize
before_action :authorize_admin_project! before_action :authorize_admin_project!
@ -26,8 +27,8 @@ class Projects::ServicesController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html do format.html do
if saved if saved
redirect_to project_settings_integrations_path(@project), target_url = safe_redirect_path(params[:redirect_to]).presence || project_settings_integrations_path(@project)
notice: success_message redirect_to target_url, notice: success_message
else else
render 'edit' render 'edit'
end end

View file

@ -12,7 +12,8 @@ module Types
description: 'Name of the link' description: 'Name of the link'
field :url, GraphQL::STRING_TYPE, null: true, field :url, GraphQL::STRING_TYPE, null: true,
description: 'URL of the link' 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?, field :external, GraphQL::BOOLEAN_TYPE, null: true, method: :external?,
description: 'Indicates the link points to an external resource' description: 'Indicates the link points to an external resource'
end end

View file

@ -0,0 +1,12 @@
# 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

View file

@ -688,6 +688,10 @@ module Ci
job_artifacts.any? job_artifacts.any?
end end
def has_test_reports?
job_artifacts.test_reports.exists?
end
def has_old_trace? def has_old_trace?
old_trace.present? old_trace.present?
end end

View file

@ -11,5 +11,35 @@ module Ci
validates :build, :project, presence: true validates :build, :project, presence: true
validates :data, json_schema: { filename: "build_report_result_data" } 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
end end

View file

@ -0,0 +1,36 @@
# 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

View file

@ -4,8 +4,6 @@
# Example: # Example:
# ``` # ```
# class DummyService # class DummyService
# prepend Measurable
#
# def execute # def execute
# # ... # # ...
# end # end

View file

@ -3,9 +3,9 @@
"type": "object", "type": "object",
"properties": { "properties": {
"coverage": { "type": "float" }, "coverage": { "type": "float" },
"junit": { "tests": {
"type": "object", "type": "object",
"items": { "$ref": "./build_report_result_data_junit.json" } "items": { "$ref": "./build_report_result_data_tests.json" }
} }
}, },
"additionalProperties": false "additionalProperties": false

View file

@ -1,5 +1,5 @@
{ {
"description": "Build report result data junit", "description": "Build report result data tests",
"type": "object", "type": "object",
"properties": { "properties": {
"name": { "type": "string" }, "name": { "type": "string" },

View file

@ -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| = 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 = render 'shared/service_settings', form: form, service: @service
.footer-block.row-content-block .footer-block.row-content-block
%input{ id: 'services_redirect_to', type: 'hidden', name: 'redirect_to', value: request.referrer }
= service_save_button = service_save_button
&nbsp; &nbsp;
= link_to _('Cancel'), project_settings_integrations_path(@project), class: 'btn btn-cancel' = link_to _('Cancel'), project_settings_integrations_path(@project), class: 'btn btn-cancel'

View file

@ -803,6 +803,14 @@
:weight: 1 :weight: 1
:idempotent: :idempotent:
:tags: [] :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 - :name: pipeline_background:ci_build_trace_chunk_flush
:feature_category: :continuous_integration :feature_category: :continuous_integration
:has_external_dependencies: :has_external_dependencies:

View file

@ -28,6 +28,7 @@ class BuildFinishedWorker # rubocop:disable Scalability/IdempotentWorker
# We execute these in sync to reduce IO. # We execute these in sync to reduce IO.
BuildTraceSectionsWorker.new.perform(build.id) BuildTraceSectionsWorker.new.perform(build.id)
BuildCoverageWorker.new.perform(build.id) BuildCoverageWorker.new.perform(build.id)
Ci::BuildReportResultWorker.new.perform(build.id)
# We execute these async as these are independent operations. # We execute these async as these are independent operations.
BuildHooksWorker.perform_async(build.id) BuildHooksWorker.perform_async(build.id)

View file

@ -0,0 +1,16 @@
# 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

View file

@ -21,10 +21,6 @@ module Secpick
@options = self.class.options @options = self.class.options
end end
def ee?
File.exist?(File.expand_path('../ee/app/models/license.rb', __dir__))
end
def dry_run? def dry_run?
@options[:try] == true @options[:try] == true
end end
@ -40,9 +36,7 @@ module Secpick
end end
def stable_branch def stable_branch
"#{@options[:version]}-#{STABLE_SUFFIX}".tap do |name| "#{@options[:version]}-#{STABLE_SUFFIX}-ee".freeze
name << "-ee" if ee?
end.freeze
end end
def git_commands def git_commands
@ -64,11 +58,7 @@ module Secpick
end end
def new_mr_url def new_mr_url
if ee? SECURITY_MR_URL
SECURITY_MR_URL
else
SECURITY_MR_URL.sub('/gitlab/', '/gitlab-foss/')
end
end end
def create! def create!

View file

@ -0,0 +1,5 @@
---
title: Add api.js methods to update issues and merge requests
merge_request: 32893
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Expose `release_links.type` via API
merge_request: 33154
author:
type: changed

View file

@ -0,0 +1,5 @@
---
title: Add `link_type` to `ReleaseLink` GraphQL type
merge_request: 33386
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Add opacity transition to active design discussion pins
merge_request: 33493
author:
type: other

View file

@ -0,0 +1,5 @@
---
title: Update deprecated slot syntax in app/assets/javascripts/reports/components/grouped_test_reports_app.vue
merge_request: 31975
author: Gilang Gumilar
type: other

View file

@ -9648,6 +9648,11 @@ type ReleaseLink {
""" """
id: ID! id: ID!
"""
Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`
"""
linkType: ReleaseLinkType
""" """
Name of the link Name of the link
""" """
@ -9694,6 +9699,31 @@ type ReleaseLinkEdge {
node: ReleaseLink 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 { type ReleaseSource {
""" """
Format of the source Format of the source

View file

@ -28194,6 +28194,20 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "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", "name": "name",
"description": "Name of the link", "description": "Name of the link",
@ -28342,6 +28356,41 @@
"enumValues": null, "enumValues": null,
"possibleTypes": 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", "kind": "OBJECT",
"name": "ReleaseSource", "name": "ReleaseSource",

View file

@ -1342,6 +1342,7 @@ Information about pagination in a connection.
| --- | ---- | ---------- | | --- | ---- | ---------- |
| `external` | Boolean | Indicates the link points to an external resource | | `external` | Boolean | Indicates the link points to an external resource |
| `id` | ID! | ID of the link | | `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 | | `name` | String | Name of the link |
| `url` | String | URL of the link | | `url` | String | URL of the link |

View file

@ -42,6 +42,7 @@ GitLab provides built-in tools to help improve performance and availability:
- [Request Profiling](../administration/monitoring/performance/request_profiling.md). - [Request Profiling](../administration/monitoring/performance/request_profiling.md).
- [QueryRecoder](query_recorder.md) for preventing `N+1` regressions. - [QueryRecoder](query_recorder.md) for preventing `N+1` regressions.
- [Chaos endpoints](chaos_endpoints.md) for testing failure scenarios. Intended mainly for testing availability. - [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 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 <https://dashboards.gitlab.net>, this requires you to log in using your

View file

@ -0,0 +1,81 @@
# 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
```

View file

@ -9,6 +9,7 @@ module API
expose :url expose :url
expose :direct_asset_url expose :direct_asset_url
expose :external?, as: :external expose :external?, as: :external
expose :link_type
def direct_asset_url def direct_asset_url
return object.url unless object.filepath return object.url unless object.filepath

View file

@ -40,6 +40,7 @@ module API
requires :name, type: String, desc: 'The name of the link' requires :name, type: String, desc: 'The name of the link'
requires :url, type: String, desc: 'The URL of the link' requires :url, type: String, desc: 'The URL of the link'
optional :filepath, type: String, desc: 'The filepath of the link' optional :filepath, type: String, desc: 'The filepath of the link'
optional :link_type, type: String, desc: 'The link type'
end end
post 'links' do post 'links' do
authorize! :create_release, release authorize! :create_release, release
@ -75,6 +76,7 @@ module API
optional :name, type: String, desc: 'The name of the link' optional :name, type: String, desc: 'The name of the link'
optional :url, type: String, desc: 'The URL of the link' optional :url, type: String, desc: 'The URL of the link'
optional :filepath, type: String, desc: 'The filepath 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 at_least_one_of :name, :url
end end
put do put do

View file

@ -134,24 +134,50 @@ describe Projects::ServicesController do
describe 'PUT #update' do describe 'PUT #update' do
describe 'as HTML' do describe 'as HTML' do
let(:service_params) { { active: true } } 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 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 end
context 'when param `active` is set to true' do context 'when param `active` is set to true' do
it 'activates the service and redirects to integrations paths' do let(:params) { project_params(service: service_params, redirect_to: redirect) }
expect(response).to redirect_to(project_settings_integrations_path(project))
expect(flash[:notice]).to eq 'Jira activated.' 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
end end
context 'when param `active` is set to false' do context 'when param `active` is set to false' do
let(:service_params) { { active: false } } let(:service_params) { { active: false } }
let(:message) { 'Jira settings saved, but not activated.' }
it 'does not activate the service but saves the settings' do it_behaves_like 'service update'
expect(flash[:notice]).to eq 'Jira settings saved, but not activated.'
end
end end
end end

View file

@ -6,7 +6,7 @@ FactoryBot.define do
project factory: :project project factory: :project
data do data do
{ {
junit: { tests: {
name: "rspec", name: "rspec",
duration: 0.42, duration: 0.42,
failed: 0, failed: 0,
@ -20,7 +20,7 @@ FactoryBot.define do
trait :with_junit_success do trait :with_junit_success do
data do data do
{ {
junit: { tests: {
name: "rspec", name: "rspec",
duration: 0.42, duration: 0.42,
failed: 0, failed: 0,

View file

@ -6,5 +6,6 @@ FactoryBot.define do
sequence(:name) { |n| "release-18.#{n}.dmg" } sequence(:name) { |n| "release-18.#{n}.dmg" }
sequence(:url) { |n| "https://example.com/scrambled-url/app-#{n}.zip" } sequence(:url) { |n| "https://example.com/scrambled-url/app-#{n}.zip" }
sequence(:filepath) { |n| "/binaries/awesome-app-#{n}" } sequence(:filepath) { |n| "/binaries/awesome-app-#{n}" }
link_type { 'other' }
end end
end end

View file

@ -7,7 +7,8 @@
"filepath": { "type": "string" }, "filepath": { "type": "string" },
"url": { "type": "string" }, "url": { "type": "string" },
"direct_asset_url": { "type": "string" }, "direct_asset_url": { "type": "string" },
"external": { "type": "boolean" } "external": { "type": "boolean" },
"link_type": { "type": "string" }
}, },
"additionalProperties": false "additionalProperties": false
} }

View file

@ -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);
});
});
}); });

View file

@ -1,9 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // 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 <button
aria-label="Comment form position" 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;" style="left: 10px; top: 10px; cursor: move;"
type="button" type="button"
> >
@ -14,10 +14,10 @@ exports[`Design discussions component should match the snapshot of note when rep
</button> </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 <button
aria-label="Comment '1' position" 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;" style="left: 10px; top: 10px;"
type="button" type="button"
> >
@ -27,10 +27,10 @@ exports[`Design discussions component should match the snapshot of note with ind
</button> </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 <button
aria-label="Comment form position" 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;" style="left: 10px; top: 10px;"
type="button" type="button"
> >

View file

@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import DesignNotePin from '~/design_management/components/design_note_pin.vue'; import DesignNotePin from '~/design_management/components/design_note_pin.vue';
describe('Design discussions component', () => { describe('Design note pin component', () => {
let wrapper; let wrapper;
function createComponent(propsData = {}) { function createComponent(propsData = {}) {

View file

@ -10,12 +10,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
</linearGradient> </linearGradient>
<clipPath id=\\"dag-clip63\\"> <clipPath id=\\"dag-clip63\\">
<path d=\\" <path d=\\"
M100, 129 M100, 129
V158 V158
H377.3333333333333 H377.3333333333333
V100 V100
H100 H100
Z\\"></path> Z
\\"></path>
</clipPath> </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> <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> </g>
@ -26,12 +27,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
</linearGradient> </linearGradient>
<clipPath id=\\"dag-clip64\\"> <clipPath id=\\"dag-clip64\\">
<path d=\\" <path d=\\"
M361.3333333333333, 129.0000000000002 M361.3333333333333, 129.0000000000002
V158.0000000000002 V158.0000000000002
H638.6666666666666 H638.6666666666666
V100 V100
H361.3333333333333 H361.3333333333333
Z\\"></path> Z
\\"></path>
</clipPath> </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> <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> </g>
@ -42,12 +44,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
</linearGradient> </linearGradient>
<clipPath id=\\"dag-clip65\\"> <clipPath id=\\"dag-clip65\\">
<path d=\\" <path d=\\"
M100, 187.0000000000002 M100, 187.0000000000002
V241.00000000000003 V241.00000000000003
H638.6666666666666 H638.6666666666666
V158.0000000000002 V158.0000000000002
H100 H100
Z\\"></path> Z
\\"></path>
</clipPath> </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> <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> </g>
@ -58,12 +61,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
</linearGradient> </linearGradient>
<clipPath id=\\"dag-clip66\\"> <clipPath id=\\"dag-clip66\\">
<path d=\\" <path d=\\"
M100, 269.9999999999998 M100, 269.9999999999998
V324 V324
H377.3333333333333 H377.3333333333333
V240.99999999999977 V240.99999999999977
H100 H100
Z\\"></path> Z
\\"></path>
</clipPath> </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> <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> </g>
@ -74,12 +78,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
</linearGradient> </linearGradient>
<clipPath id=\\"dag-clip67\\"> <clipPath id=\\"dag-clip67\\">
<path d=\\" <path d=\\"
M100, 352.99999999999994 M100, 352.99999999999994
V407.00000000000006 V407.00000000000006
H377.3333333333333 H377.3333333333333
V323.99999999999994 V323.99999999999994
H100 H100
Z\\"></path> Z
\\"></path>
</clipPath> </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> <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> </g>
@ -90,12 +95,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
</linearGradient> </linearGradient>
<clipPath id=\\"dag-clip68\\"> <clipPath id=\\"dag-clip68\\">
<path d=\\" <path d=\\"
M361.3333333333333, 270.0000000000001 M361.3333333333333, 270.0000000000001
V299.0000000000001 V299.0000000000001
H638.6666666666666 H638.6666666666666
V240.99999999999977 V240.99999999999977
H361.3333333333333 H361.3333333333333
Z\\"></path> Z
\\"></path>
</clipPath> </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> <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> </g>
@ -106,12 +112,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
</linearGradient> </linearGradient>
<clipPath id=\\"dag-clip69\\"> <clipPath id=\\"dag-clip69\\">
<path d=\\" <path d=\\"
M361.3333333333333, 328.0000000000001 M361.3333333333333, 328.0000000000001
V381.99999999999994 V381.99999999999994
H638.6666666666666 H638.6666666666666
V299.0000000000001 V299.0000000000001
H361.3333333333333 H361.3333333333333
Z\\"></path> Z
\\"></path>
</clipPath> </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> <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> </g>
@ -122,12 +129,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
</linearGradient> </linearGradient>
<clipPath id=\\"dag-clip70\\"> <clipPath id=\\"dag-clip70\\">
<path d=\\" <path d=\\"
M361.3333333333333, 411 M361.3333333333333, 411
V440 V440
H638.6666666666666 H638.6666666666666
V381.99999999999994 V381.99999999999994
H361.3333333333333 H361.3333333333333
Z\\"></path> Z
\\"></path>
</clipPath> </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> <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> </g>
@ -138,12 +146,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
</linearGradient> </linearGradient>
<clipPath id=\\"dag-clip71\\"> <clipPath id=\\"dag-clip71\\">
<path d=\\" <path d=\\"
M622.6666666666666, 270.1890725105691 M622.6666666666666, 270.1890725105691
V299.1890725105691 V299.1890725105691
H900 H900
V241.0000000000001 V241.0000000000001
H622.6666666666666 H622.6666666666666
Z\\"></path> Z
\\"></path>
</clipPath> </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> <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> </g>
@ -154,12 +163,13 @@ exports[`The DAG graph in the basic case renders the graph svg 1`] = `
</linearGradient> </linearGradient>
<clipPath id=\\"dag-clip72\\"> <clipPath id=\\"dag-clip72\\">
<path d=\\" <path d=\\"
M622.6666666666666, 411 M622.6666666666666, 411
V440 V440
H900 H900
V382 V382
H622.6666666666666 H622.6666666666666
Z\\"></path> Z
\\"></path>
</clipPath> </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> <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> </g>

View file

@ -1,6 +1,7 @@
import { mount } from '@vue/test-utils'; import { mount } from '@vue/test-utils';
import DagGraph from '~/pipelines/components/dag/dag_graph.vue'; 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'; import { parsedData } from './mock_data';
describe('The DAG graph', () => { describe('The DAG graph', () => {

View file

@ -0,0 +1,57 @@
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();
});
});
});
});

View file

@ -3,11 +3,11 @@ import {
makeLinksFromNodes, makeLinksFromNodes,
filterByAncestors, filterByAncestors,
parseData, parseData,
createSankey,
removeOrphanNodes, removeOrphanNodes,
getMaxNodes, 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'; import { mockBaseData } from './mock_data';
describe('DAG visualization parsing utilities', () => { 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', () => { describe('removeOrphanNodes', () => {
it('removes sankey nodes that have no needs and are not needed', () => { it('removes sankey nodes that have no needs and are not needed', () => {
const cleanedNodes = removeOrphanNodes(sankeyLayout.nodes); const cleanedNodes = removeOrphanNodes(sankeyLayout.nodes);

View file

@ -7,7 +7,7 @@ describe GitlabSchema.types['ReleaseLink'] do
it 'has the expected fields' do it 'has the expected fields' do
expected_fields = %w[ expected_fields = %w[
id name url external id name url external link_type
] ]
expect(described_class).to include_graphql_fields(*expected_fields) expect(described_class).to include_graphql_fields(*expected_fields)

View file

@ -29,4 +29,46 @@ describe Ci::BuildReportResult do
end end
end 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 end

View file

@ -875,6 +875,22 @@ describe Ci::Build do
end end
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 describe '#has_old_trace?' do
subject { build.has_old_trace? } subject { build.has_old_trace? }

View file

@ -0,0 +1,51 @@
# 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

View file

@ -15,7 +15,8 @@ describe 'projects/services/_form' do
allow(view).to receive_messages(current_user: user, allow(view).to receive_messages(current_user: user,
can?: true, can?: true,
current_application_settings: Gitlab::CurrentSettings.current_application_settings) current_application_settings: Gitlab::CurrentSettings.current_application_settings,
request: double(referrer: '/services'))
end end
context 'commit_events and merge_request_events' do 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 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_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 end
end end

View file

@ -3,7 +3,11 @@
require 'spec_helper' require 'spec_helper'
describe BuildFinishedWorker do describe BuildFinishedWorker do
subject { described_class.new.perform(build.id) }
describe '#perform' do describe '#perform' do
let(:build) { create(:ci_build, :success, pipeline: create(:ci_pipeline)) }
context 'when build exists' do context 'when build exists' do
let!(:build) { create(:ci_build) } let!(:build) { create(:ci_build) }
@ -18,8 +22,10 @@ describe BuildFinishedWorker do
expect(BuildHooksWorker).to receive(:perform_async) expect(BuildHooksWorker).to receive(:perform_async)
expect(ArchiveTraceWorker).to receive(:perform_async) expect(ArchiveTraceWorker).to receive(:perform_async)
expect(ExpirePipelineCacheWorker).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
end end
@ -30,23 +36,26 @@ describe BuildFinishedWorker do
end end
end end
it 'schedules a ChatNotification job for a chat build' do context 'when build has a chat' do
build = create(:ci_build, :success, pipeline: create(:ci_pipeline, source: :chat)) let(:build) { create(:ci_build, :success, pipeline: create(:ci_pipeline, source: :chat)) }
expect(ChatNotificationWorker) it 'schedules a ChatNotification job' do
.to receive(:perform_async) expect(ChatNotificationWorker).to receive(:perform_async).with(build.id)
.with(build.id)
described_class.new.perform(build.id) subject
end
end end
it 'does not schedule a ChatNotification job for a regular build' do context 'when build has a test report' do
build = create(:ci_build, :success, pipeline: create(:ci_pipeline)) let(:build) { create(:ci_build, :test_reports) }
expect(ChatNotificationWorker) it 'schedules a BuildReportResult job' do
.not_to receive(:perform_async) 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 end
end end

View file

@ -0,0 +1,30 @@
# 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