summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-06-02 21:08:00 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-06-02 21:08:00 +0000
commit2164573e4531de7949b0ad9fe1d55bfb9c42765d (patch)
tree10cf954a1225eb3162009f5c2457bacdc388aa63
parentf3e7bc80608c100227030030a6a601897f8e4ff9 (diff)
downloadgitlab-ce-2164573e4531de7949b0ad9fe1d55bfb9c42765d.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/api.js17
-rw-r--r--app/assets/javascripts/design_management/components/design_note_pin.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/dag/dag.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/dag/dag_graph.vue117
-rw-r--r--app/assets/javascripts/pipelines/components/dag/drawing_utils.js134
-rw-r--r--app/assets/javascripts/pipelines/components/dag/parsing_utils.js (renamed from app/assets/javascripts/pipelines/components/dag/utils.js)29
-rw-r--r--app/assets/javascripts/reports/components/grouped_test_reports_app.vue40
-rw-r--r--app/assets/stylesheets/components/avatar.scss11
-rw-r--r--app/assets/stylesheets/components/design_management/design.scss12
-rw-r--r--app/assets/stylesheets/framework/animations.scss1
-rw-r--r--app/controllers/projects/services_controller.rb5
-rw-r--r--app/graphql/types/release_link_type.rb3
-rw-r--r--app/graphql/types/release_link_type_enum.rb12
-rw-r--r--app/models/ci/build.rb4
-rw-r--r--app/models/ci/build_report_result.rb30
-rw-r--r--app/services/ci/build_report_result_service.rb36
-rw-r--r--app/services/concerns/measurable.rb2
-rw-r--r--app/validators/json_schemas/build_report_result_data.json4
-rw-r--r--app/validators/json_schemas/build_report_result_data_tests.json (renamed from app/validators/json_schemas/build_report_result_data_junit.json)2
-rw-r--r--app/views/projects/services/_form.html.haml1
-rw-r--r--app/workers/all_queues.yml8
-rw-r--r--app/workers/build_finished_worker.rb1
-rw-r--r--app/workers/ci/build_report_result_worker.rb16
-rwxr-xr-xbin/secpick14
-rw-r--r--changelogs/unreleased/191455-add-a-button-to-quickly-assign-users-who-have-commented-on-an-issu.yml5
-rw-r--r--changelogs/unreleased/207257-specify-asset-types-in-releases-2.yml5
-rw-r--r--changelogs/unreleased/207257-specify-asset-types-in-releases-3.yml5
-rw-r--r--changelogs/unreleased/217692-design-view-highlight-focused-design-pins-follow-up.yml5
-rw-r--r--changelogs/unreleased/update-deprecated-slot-syntax-in-app-assets-javascripts-reports-component.yml5
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql30
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json49
-rw-r--r--doc/api/graphql/reference/index.md1
-rw-r--r--doc/development/performance.md1
-rw-r--r--doc/development/service_measurement.md81
-rw-r--r--lib/api/entities/releases/link.rb1
-rw-r--r--lib/api/release/links.rb2
-rw-r--r--spec/controllers/projects/services_controller_spec.rb40
-rw-r--r--spec/factories/ci/build_report_results.rb4
-rw-r--r--spec/factories/releases/link.rb1
-rw-r--r--spec/fixtures/api/schemas/release/link.json3
-rw-r--r--spec/frontend/api_spec.js34
-rw-r--r--spec/frontend/design_management/components/__snapshots__/design_note_pin_spec.js.snap12
-rw-r--r--spec/frontend/design_management/components/design_note_pin_spec.js2
-rw-r--r--spec/frontend/pipelines/components/dag/__snapshots__/dag_graph_spec.js.snap130
-rw-r--r--spec/frontend/pipelines/components/dag/dag_graph_spec.js3
-rw-r--r--spec/frontend/pipelines/components/dag/drawing_utils_spec.js57
-rw-r--r--spec/frontend/pipelines/components/dag/parsing_utils_spec.js (renamed from spec/frontend/pipelines/components/dag/utils_spec.js)42
-rw-r--r--spec/graphql/types/release_links_type_spec.rb2
-rw-r--r--spec/models/ci/build_report_result_spec.rb42
-rw-r--r--spec/models/ci/build_spec.rb16
-rw-r--r--spec/services/ci/build_report_result_service_spec.rb51
-rw-r--r--spec/views/projects/services/_form.haml_spec.rb4
-rw-r--r--spec/workers/build_finished_worker_spec.rb33
-rw-r--r--spec/workers/ci/build_report_result_worker_spec.rb30
54 files changed, 878 insertions, 321 deletions
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index e527659a939..41d59f22224 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -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));
},
diff --git a/app/assets/javascripts/design_management/components/design_note_pin.vue b/app/assets/javascripts/design_management/components/design_note_pin.vue
index 50ea69d52ce..7827f2ce204 100644
--- a/app/assets/javascripts/design_management/components/design_note_pin.vue
+++ b/app/assets/javascripts/design_management/components/design_note_pin.vue
@@ -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)"
diff --git a/app/assets/javascripts/pipelines/components/dag/dag.vue b/app/assets/javascripts/pipelines/components/dag/dag.vue
index 13cbf47ad4f..3547909ac7d 100644
--- a/app/assets/javascripts/pipelines/components/dag/dag.vue
+++ b/app/assets/javascripts/pipelines/components/dag/dag.vue
@@ -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
diff --git a/app/assets/javascripts/pipelines/components/dag/dag_graph.vue b/app/assets/javascripts/pipelines/components/dag/dag_graph.vue
index c6187827510..d91fe9bb7f4 100644
--- a/app/assets/javascripts/pipelines/components/dag/dag_graph.vue
+++ b/app/assets/javascripts/pipelines/components/dag/dag_graph.vue
@@ -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);
diff --git a/app/assets/javascripts/pipelines/components/dag/drawing_utils.js b/app/assets/javascripts/pipelines/components/dag/drawing_utils.js
new file mode 100644
index 00000000000..d56addc473f
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/dag/drawing_utils.js
@@ -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',
+ };
+};
diff --git a/app/assets/javascripts/pipelines/components/dag/utils.js b/app/assets/javascripts/pipelines/components/dag/parsing_utils.js
index 76cfd75f9fd..3234f80ee91 100644
--- a/app/assets/javascripts/pipelines/components/dag/utils.js
+++ b/app/assets/javascripts/pipelines/components/dag/parsing_utils.js
@@ -1,4 +1,3 @@
-import { sankey, sankeyLeft } from 'd3-sankey';
import { uniqWith, isEqual } from 'lodash';
/*
@@ -137,34 +136,6 @@ export const parseData = data => {
};
/*
- 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.
*/
diff --git a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
index 0f7a0e60dc0..a670cad5f9f 100644
--- a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
+++ b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue
@@ -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>
diff --git a/app/assets/stylesheets/components/avatar.scss b/app/assets/stylesheets/components/avatar.scss
index 312123aeef9..6bb7e9d215e 100644
--- a/app/assets/stylesheets/components/avatar.scss
+++ b/app/assets/stylesheets/components/avatar.scss
@@ -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;
diff --git a/app/assets/stylesheets/components/design_management/design.scss b/app/assets/stylesheets/components/design_management/design.scss
index 0b5c32021b8..8a83a1612ff 100644
--- a/app/assets/stylesheets/components/design_management/design.scss
+++ b/app/assets/stylesheets/components/design_management/design.scss
@@ -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;
+ }
+ }
}
}
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index 566ab5bc5c4..136ff82e0f8 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -93,7 +93,6 @@
}
.dropdown-menu-toggle,
-.avatar-circle,
.header-user-avatar {
@include transition(border-color);
}
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index 92c6ce324f7..5ed9baf04f6 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -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
diff --git a/app/graphql/types/release_link_type.rb b/app/graphql/types/release_link_type.rb
index c7a1d3f3767..070f14a90df 100644
--- a/app/graphql/types/release_link_type.rb
+++ b/app/graphql/types/release_link_type.rb
@@ -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
diff --git a/app/graphql/types/release_link_type_enum.rb b/app/graphql/types/release_link_type_enum.rb
new file mode 100644
index 00000000000..b364855833f
--- /dev/null
+++ b/app/graphql/types/release_link_type_enum.rb
@@ -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
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index e1d53a700ec..8d4fe633aa4 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -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
diff --git a/app/models/ci/build_report_result.rb b/app/models/ci/build_report_result.rb
index a65ef885b88..530233ad5c0 100644
--- a/app/models/ci/build_report_result.rb
+++ b/app/models/ci/build_report_result.rb
@@ -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
diff --git a/app/services/ci/build_report_result_service.rb b/app/services/ci/build_report_result_service.rb
new file mode 100644
index 00000000000..758ba1c73bf
--- /dev/null
+++ b/app/services/ci/build_report_result_service.rb
@@ -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
diff --git a/app/services/concerns/measurable.rb b/app/services/concerns/measurable.rb
index 5a74f15506e..b099a58a9ae 100644
--- a/app/services/concerns/measurable.rb
+++ b/app/services/concerns/measurable.rb
@@ -4,8 +4,6 @@
# Example:
# ```
# class DummyService
-# prepend Measurable
-#
# def execute
# # ...
# end
diff --git a/app/validators/json_schemas/build_report_result_data.json b/app/validators/json_schemas/build_report_result_data.json
index b364266b9d6..0fb4fd6d0b7 100644
--- a/app/validators/json_schemas/build_report_result_data.json
+++ b/app/validators/json_schemas/build_report_result_data.json
@@ -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
diff --git a/app/validators/json_schemas/build_report_result_data_junit.json b/app/validators/json_schemas/build_report_result_data_tests.json
index f69cbd4f16d..b38559e727f 100644
--- a/app/validators/json_schemas/build_report_result_data_junit.json
+++ b/app/validators/json_schemas/build_report_result_data_tests.json
@@ -1,5 +1,5 @@
{
- "description": "Build report result data junit",
+ "description": "Build report result data tests",
"type": "object",
"properties": {
"name": { "type": "string" },
diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml
index 05c49a7b6ce..e6761807409 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/services/_form.html.haml
@@ -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'
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 0dded4d6474..8f3f14c75f0 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -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:
diff --git a/app/workers/build_finished_worker.rb b/app/workers/build_finished_worker.rb
index b6ef9ab4710..d38780dd08d 100644
--- a/app/workers/build_finished_worker.rb
+++ b/app/workers/build_finished_worker.rb
@@ -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)
diff --git a/app/workers/ci/build_report_result_worker.rb b/app/workers/ci/build_report_result_worker.rb
new file mode 100644
index 00000000000..60387936d0b
--- /dev/null
+++ b/app/workers/ci/build_report_result_worker.rb
@@ -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
diff --git a/bin/secpick b/bin/secpick
index a68dabc8c47..4d056ceecaf 100755
--- a/bin/secpick
+++ b/bin/secpick
@@ -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!
diff --git a/changelogs/unreleased/191455-add-a-button-to-quickly-assign-users-who-have-commented-on-an-issu.yml b/changelogs/unreleased/191455-add-a-button-to-quickly-assign-users-who-have-commented-on-an-issu.yml
new file mode 100644
index 00000000000..df303695865
--- /dev/null
+++ b/changelogs/unreleased/191455-add-a-button-to-quickly-assign-users-who-have-commented-on-an-issu.yml
@@ -0,0 +1,5 @@
+---
+title: Add api.js methods to update issues and merge requests
+merge_request: 32893
+author:
+type: added
diff --git a/changelogs/unreleased/207257-specify-asset-types-in-releases-2.yml b/changelogs/unreleased/207257-specify-asset-types-in-releases-2.yml
new file mode 100644
index 00000000000..f897faeeaa2
--- /dev/null
+++ b/changelogs/unreleased/207257-specify-asset-types-in-releases-2.yml
@@ -0,0 +1,5 @@
+---
+title: Expose `release_links.type` via API
+merge_request: 33154
+author:
+type: changed
diff --git a/changelogs/unreleased/207257-specify-asset-types-in-releases-3.yml b/changelogs/unreleased/207257-specify-asset-types-in-releases-3.yml
new file mode 100644
index 00000000000..bf5de47e7ff
--- /dev/null
+++ b/changelogs/unreleased/207257-specify-asset-types-in-releases-3.yml
@@ -0,0 +1,5 @@
+---
+title: Add `link_type` to `ReleaseLink` GraphQL type
+merge_request: 33386
+author:
+type: added
diff --git a/changelogs/unreleased/217692-design-view-highlight-focused-design-pins-follow-up.yml b/changelogs/unreleased/217692-design-view-highlight-focused-design-pins-follow-up.yml
new file mode 100644
index 00000000000..27b6d0dd58c
--- /dev/null
+++ b/changelogs/unreleased/217692-design-view-highlight-focused-design-pins-follow-up.yml
@@ -0,0 +1,5 @@
+---
+title: Add opacity transition to active design discussion pins
+merge_request: 33493
+author:
+type: other
diff --git a/changelogs/unreleased/update-deprecated-slot-syntax-in-app-assets-javascripts-reports-component.yml b/changelogs/unreleased/update-deprecated-slot-syntax-in-app-assets-javascripts-reports-component.yml
new file mode 100644
index 00000000000..b880020aa7f
--- /dev/null
+++ b/changelogs/unreleased/update-deprecated-slot-syntax-in-app-assets-javascripts-reports-component.yml
@@ -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
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index c3fd02d8c9c..626b99eeecb 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -9649,6 +9649,11 @@ type ReleaseLink {
id: ID!
"""
+ Type of the link: `other`, `runbook`, `image`, `package`; defaults to `other`
+ """
+ linkType: ReleaseLinkType
+
+ """
Name of the link
"""
name: String
@@ -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
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 2ac543cb14a..dc1e909384e 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -28195,6 +28195,20 @@
"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",
"args": [
@@ -28343,6 +28357,41 @@
"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",
"description": null,
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 452d0059cca..6a6427948dc 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -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 |
diff --git a/doc/development/performance.md b/doc/development/performance.md
index 050444c57af..69ad524675d 100644
--- a/doc/development/performance.md
+++ b/doc/development/performance.md
@@ -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
diff --git a/doc/development/service_measurement.md b/doc/development/service_measurement.md
new file mode 100644
index 00000000000..bc3f3534cc6
--- /dev/null
+++ b/doc/development/service_measurement.md
@@ -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
+```
diff --git a/lib/api/entities/releases/link.rb b/lib/api/entities/releases/link.rb
index f4edb83bd58..654df2e2caf 100644
--- a/lib/api/entities/releases/link.rb
+++ b/lib/api/entities/releases/link.rb
@@ -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
diff --git a/lib/api/release/links.rb b/lib/api/release/links.rb
index f72230c084c..07c27f39539 100644
--- a/lib/api/release/links.rb
+++ b/lib/api/release/links.rb
@@ -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
diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb
index c669119fa4e..b591e52d45c 100644
--- a/spec/controllers/projects/services_controller_spec.rb
+++ b/spec/controllers/projects/services_controller_spec.rb
@@ -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
diff --git a/spec/factories/ci/build_report_results.rb b/spec/factories/ci/build_report_results.rb
index 00009ead126..0685c0e5554 100644
--- a/spec/factories/ci/build_report_results.rb
+++ b/spec/factories/ci/build_report_results.rb
@@ -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,
diff --git a/spec/factories/releases/link.rb b/spec/factories/releases/link.rb
index 001deeb71a0..da0efe4a749 100644
--- a/spec/factories/releases/link.rb
+++ b/spec/factories/releases/link.rb
@@ -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
diff --git a/spec/fixtures/api/schemas/release/link.json b/spec/fixtures/api/schemas/release/link.json
index bf175be2bc0..b3aebfa131e 100644
--- a/spec/fixtures/api/schemas/release/link.json
+++ b/spec/fixtures/api/schemas/release/link.json
@@ -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
}
diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js
index d365048ab0b..d3659693405 100644
--- a/spec/frontend/api_spec.js
+++ b/spec/frontend/api_spec.js
@@ -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);
+ });
+ });
});
diff --git a/spec/frontend/design_management/components/__snapshots__/design_note_pin_spec.js.snap b/spec/frontend/design_management/components/__snapshots__/design_note_pin_spec.js.snap
index 4828e8cb3c2..4c848256e5b 100644
--- a/spec/frontend/design_management/components/__snapshots__/design_note_pin_spec.js.snap
+++ b/spec/frontend/design_management/components/__snapshots__/design_note_pin_spec.js.snap
@@ -1,9 +1,9 @@
// 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"
>
diff --git a/spec/frontend/design_management/components/design_note_pin_spec.js b/spec/frontend/design_management/components/design_note_pin_spec.js
index 4f7260b1363..76fe5402053 100644
--- a/spec/frontend/design_management/components/design_note_pin_spec.js
+++ b/spec/frontend/design_management/components/design_note_pin_spec.js
@@ -1,7 +1,7 @@
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 = {}) {
diff --git a/spec/frontend/pipelines/components/dag/__snapshots__/dag_graph_spec.js.snap b/spec/frontend/pipelines/components/dag/__snapshots__/dag_graph_spec.js.snap
index 5390c2f8e0c..629efc6d3fa 100644
--- a/spec/frontend/pipelines/components/dag/__snapshots__/dag_graph_spec.js.snap
+++ b/spec/frontend/pipelines/components/dag/__snapshots__/dag_graph_spec.js.snap
@@ -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>
diff --git a/spec/frontend/pipelines/components/dag/dag_graph_spec.js b/spec/frontend/pipelines/components/dag/dag_graph_spec.js
index bc576397967..a6f712b1984 100644
--- a/spec/frontend/pipelines/components/dag/dag_graph_spec.js
+++ b/spec/frontend/pipelines/components/dag/dag_graph_spec.js
@@ -1,6 +1,7 @@
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', () => {
diff --git a/spec/frontend/pipelines/components/dag/drawing_utils_spec.js b/spec/frontend/pipelines/components/dag/drawing_utils_spec.js
new file mode 100644
index 00000000000..a50163411ed
--- /dev/null
+++ b/spec/frontend/pipelines/components/dag/drawing_utils_spec.js
@@ -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();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/components/dag/utils_spec.js b/spec/frontend/pipelines/components/dag/parsing_utils_spec.js
index be0e4f0ea8e..d9a1296e572 100644
--- a/spec/frontend/pipelines/components/dag/utils_spec.js
+++ b/spec/frontend/pipelines/components/dag/parsing_utils_spec.js
@@ -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);
diff --git a/spec/graphql/types/release_links_type_spec.rb b/spec/graphql/types/release_links_type_spec.rb
index aaedb9ea29c..49e04e120f4 100644
--- a/spec/graphql/types/release_links_type_spec.rb
+++ b/spec/graphql/types/release_links_type_spec.rb
@@ -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)
diff --git a/spec/models/ci/build_report_result_spec.rb b/spec/models/ci/build_report_result_spec.rb
index e9211a22d08..078b0d100a1 100644
--- a/spec/models/ci/build_report_result_spec.rb
+++ b/spec/models/ci/build_report_result_spec.rb
@@ -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
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index f5f2d176636..01961847806 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -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? }
diff --git a/spec/services/ci/build_report_result_service_spec.rb b/spec/services/ci/build_report_result_service_spec.rb
new file mode 100644
index 00000000000..dbdfc774314
--- /dev/null
+++ b/spec/services/ci/build_report_result_service_spec.rb
@@ -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
diff --git a/spec/views/projects/services/_form.haml_spec.rb b/spec/views/projects/services/_form.haml_spec.rb
index a3faa92b50e..720e0aaf450 100644
--- a/spec/views/projects/services/_form.haml_spec.rb
+++ b/spec/views/projects/services/_form.haml_spec.rb
@@ -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
diff --git a/spec/workers/build_finished_worker_spec.rb b/spec/workers/build_finished_worker_spec.rb
index 4adb795b1d6..849563d9608 100644
--- a/spec/workers/build_finished_worker_spec.rb
+++ b/spec/workers/build_finished_worker_spec.rb
@@ -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
diff --git a/spec/workers/ci/build_report_result_worker_spec.rb b/spec/workers/ci/build_report_result_worker_spec.rb
new file mode 100644
index 00000000000..290a98366b4
--- /dev/null
+++ b/spec/workers/ci/build_report_result_worker_spec.rb
@@ -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