Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-07-11 12:08:41 +00:00
parent 953b58d061
commit 8db5a83725
17 changed files with 428 additions and 145 deletions

View File

@ -1,10 +1,12 @@
<script>
import { GlTooltipDirective, GlIcon, GlSprintf, GlSkeletonLoader } from '@gitlab/ui';
import { GlTooltipDirective, GlIcon, GlSprintf, GlSkeletonLoader, GlButton } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { n__ } from '~/locale';
import Tracking from '~/tracking';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import { joinPaths } from '~/lib/utils/url_utility';
import {
LIST_DELETE_BUTTON_DISABLED,
LIST_DELETE_BUTTON_DISABLED_FOR_MIGRATION,
@ -14,6 +16,9 @@ import {
IMAGE_FAILED_DELETED_STATUS,
IMAGE_MIGRATING_STATE,
COPY_IMAGE_PATH_TITLE,
IMAGE_FULL_PATH_LABEL,
TRACKING_ACTION_CLICK_SHOW_FULL_PATH,
TRACKING_LABEL_REGISTRY_IMAGE_LIST,
} from '../../constants/index';
import DeleteButton from '../delete_button.vue';
import CleanupStatus from './cleanup_status.vue';
@ -24,6 +29,7 @@ export default {
ClipboardButton,
DeleteButton,
GlSprintf,
GlButton,
GlIcon,
ListItem,
GlSkeletonLoader,
@ -32,6 +38,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [Tracking.mixin(), glFeatureFlagsMixin()],
inject: ['config'],
props: {
item: {
@ -53,6 +60,12 @@ export default {
REMOVE_REPOSITORY_LABEL,
ROW_SCHEDULED_FOR_DELETION,
COPY_IMAGE_PATH_TITLE,
IMAGE_FULL_PATH_LABEL,
},
data() {
return {
showFullPath: false,
};
},
computed: {
disabledDelete() {
@ -78,6 +91,16 @@ export default {
);
},
imageName() {
if (this.glFeatures.containerRegistryShowShortenedPath) {
if (this.showFullPath) {
return this.item.path;
}
const projectPath = this.item?.project?.path ?? '';
if (this.item.name) {
return joinPaths(projectPath, this.item.name);
}
return projectPath;
}
return this.item.path;
},
routerLinkEvent() {
@ -89,6 +112,15 @@ export default {
: LIST_DELETE_BUTTON_DISABLED;
},
},
methods: {
hideButton() {
this.showFullPath = true;
this.$refs.imageName.$el.focus();
this.track(TRACKING_ACTION_CLICK_SHOW_FULL_PATH, {
label: TRACKING_LABEL_REGISTRY_IMAGE_LIST,
});
},
},
};
</script>
@ -103,7 +135,20 @@ export default {
:disabled="deleting"
>
<template #left-primary>
<gl-button
v-if="glFeatures.containerRegistryShowShortenedPath && !showFullPath"
v-gl-tooltip="{
placement: 'top',
title: $options.i18n.IMAGE_FULL_PATH_LABEL,
}"
icon="ellipsis_h"
size="small"
class="gl-mr-2"
:aria-label="$options.i18n.IMAGE_FULL_PATH_LABEL"
@click="hideButton"
/>
<router-link
ref="imageName"
class="gl-text-body gl-font-weight-bold"
data-testid="details-link"
data-qa-selector="registry_image_content"

View File

@ -43,6 +43,13 @@ export const EMPTY_RESULT_MESSAGE = s__(
export const COPY_IMAGE_PATH_TITLE = s__('ContainerRegistry|Copy image path');
export const IMAGE_FULL_PATH_LABEL = s__('ContainerRegistry|Show full path');
// Tracking
export const TRACKING_LABEL_REGISTRY_IMAGE_LIST = 'registry_image_list';
export const TRACKING_ACTION_CLICK_SHOW_FULL_PATH = 'click_show_full_path';
// Parameters
export const IMAGE_DELETE_SCHEDULED_STATUS = 'DELETE_SCHEDULED';

View File

@ -32,9 +32,6 @@ export default {
};
},
statusIcon(data) {
if (data.parsingInProgress) {
return null;
}
if (data.status === TESTS_FAILED_STATUS) {
return EXTENSION_ICONS.warning;
}
@ -56,18 +53,19 @@ export default {
},
methods: {
fetchCollapsedData() {
return axios.get(this.testResultsPath).then((res) => {
const { data = {}, status } = res;
return axios.get(this.testResultsPath).then((response) => {
const { data = {}, status } = response;
const { suites = [], summary = {} } = data;
return {
...res,
...response,
data: {
hasSuiteError: data.suites?.some((suite) => suite.status === ERROR_STATUS),
hasSuiteError: suites.some((suite) => suite.status === ERROR_STATUS),
parsingInProgress: status === 204,
...data,
summary: {
recentlyFailed: countRecentlyFailedTests(data.suites),
...data.summary,
recentlyFailed: countRecentlyFailedTests(suites),
...summary,
},
},
};

View File

@ -1,3 +1,4 @@
import { isEmpty } from 'lodash';
import { i18n } from './constants';
const textBuilder = (results, boldNumbers = false) => {
@ -65,6 +66,11 @@ export const reportSubTextBuilder = ({ suite_errors, summary }) => {
};
export const countRecentlyFailedTests = (subject) => {
// return 0 count if subject is [], null, or undefined
if (isEmpty(subject)) {
return 0;
}
// handle either a single report or an array of reports
const reports = !subject.length ? [subject] : subject;
@ -73,10 +79,10 @@ export const countRecentlyFailedTests = (subject) => {
return (
[report.new_failures, report.existing_failures, report.resolved_failures]
// only count tests which have failed more than once
.map(
(failureArray) =>
failureArray.filter((failure) => failure.recent_failures?.count > 1).length,
)
.map((failureArray) => {
if (!failureArray) return 0;
return failureArray.filter((failure) => failure.recent_failures?.count > 1).length;
})
.reduce((total, count) => total + count, 0)
);
})

View File

@ -8,6 +8,10 @@ module Groups
before_action :verify_container_registry_enabled!
before_action :authorize_read_container_image!
before_action do
push_frontend_feature_flag(:container_registry_show_shortened_path, group)
end
feature_category :container_registry
urgency :low

View File

@ -8,6 +8,10 @@ module Projects
before_action :authorize_update_container_image!, only: [:destroy]
before_action do
push_frontend_feature_flag(:container_registry_show_shortened_path, project)
end
def index
respond_to do |format|
format.html { ensure_root_container_repository! }

View File

@ -32,6 +32,10 @@ query getProjectContainerRepositories(
createdAt
expirationPolicyStartedAt
expirationPolicyCleanupStatus
project {
id
path
}
__typename
}
pageInfo {
@ -67,6 +71,10 @@ query getProjectContainerRepositories(
createdAt
expirationPolicyStartedAt
expirationPolicyCleanupStatus
project {
id
path
}
__typename
}
pageInfo {

View File

@ -4,7 +4,7 @@
%section.settings.as-performance.no-animate#js-performance-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Performance optimization')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
@ -15,7 +15,7 @@
%section.settings.as-ip-limits.no-animate#js-ip-limits-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'ip_limits_content' } }
.settings-header
%h4
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('User and IP rate limits')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
@ -27,7 +27,7 @@
%section.settings.as-packages-limits.no-animate#js-packages-limits-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'packages_limits_content' } }
.settings-header
%h4
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Package registry rate limits')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
@ -39,7 +39,7 @@
%section.settings.as-files-limits.no-animate#js-files-limits-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Files API Rate Limits')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
@ -50,7 +50,7 @@
%section.settings.as-search-limits.no-animate#js-search-limits-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Search rate limits')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
@ -61,7 +61,7 @@
%section.settings.as-deprecated-limits.no-animate#js-deprecated-limits-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Deprecated API rate limits')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
@ -73,7 +73,7 @@
%section.settings.as-git-lfs-limits.no-animate#js-git-lfs-limits-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'git_lfs_limits_content' } }
.settings-header
%h4
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Git LFS Rate Limits')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
@ -85,7 +85,7 @@
%section.settings.as-outbound.no-animate#js-outbound-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'outbound_requests_content' } }
.settings-header
%h4
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= s_('OutboundRequests|Outbound requests')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
@ -98,7 +98,7 @@
%section.settings.as-protected-paths.no-animate#js-protected-paths-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Protected paths')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
@ -111,7 +111,7 @@
%section.settings.as-issue-limits.no-animate#js-issue-limits-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Issues Rate Limits')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
@ -123,7 +123,7 @@
%section.settings.as-note-limits.no-animate#js-note-limits-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Notes rate limit')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
@ -135,7 +135,7 @@
%section.settings.as-users-api-limits.no-animate#js-users-api-limits-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Users API rate limit')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
@ -147,7 +147,7 @@
%section.settings.as-import-export-limits.no-animate#js-import-export-limits-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Import and export rate limits')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
@ -159,7 +159,7 @@
%section.settings.as-pipeline-limits.no-animate#js-pipeline-limits-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Pipeline creation rate limits')
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')

View File

@ -0,0 +1,8 @@
---
name: container_registry_show_shortened_path
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91548
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/366808
milestone: '15.2'
type: development
group: group::package
default_enabled: false

View File

@ -1,8 +0,0 @@
---
name: gitlab_sli_new_counters
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/90977
rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/1760
milestone: '15.2'
type: development
group: group::scalability
default_enabled: false

View File

@ -48,24 +48,14 @@ module Gitlab
# This module is effectively an abstract class
@initialized_with_combinations = possible_label_combinations.any? # rubocop:disable Gitlab/ModuleWithInstanceVariables
possible_label_combinations.each do |label_combination|
legacy_total_counter.get(label_combination)
legacy_numerator_counter.get(label_combination)
if ::Feature.enabled?(:gitlab_sli_new_counters)
total_counter.get(label_combination)
numerator_counter.get(label_combination)
end
total_counter.get(label_combination)
numerator_counter.get(label_combination)
end
end
def increment(labels:, increment_numerator:)
legacy_total_counter.increment(labels)
legacy_numerator_counter.increment(labels) if increment_numerator
if ::Feature.enabled?(:gitlab_sli_new_counters)
total_counter.increment(labels)
numerator_counter.increment(labels) if increment_numerator
end
total_counter.increment(labels)
numerator_counter.increment(labels) if increment_numerator
end
def initialized?
@ -75,11 +65,7 @@ module Gitlab
private
def total_counter
prometheus.counter(counter_name('total', '_'), "Total number of measurements for #{name}")
end
def legacy_total_counter
prometheus.counter(counter_name('total', ':'), "Total number of measurements for #{name}")
prometheus.counter(counter_name('total'), "Total number of measurements for #{name}")
end
def prometheus
@ -95,16 +81,12 @@ module Gitlab
private
def counter_name(suffix, separator)
[COUNTER_PREFIX, "#{name}_apdex", suffix].join(separator).to_sym
def counter_name(suffix)
[COUNTER_PREFIX, "#{name}_apdex", suffix].join('_').to_sym
end
def numerator_counter
prometheus.counter(counter_name('success_total', '_'), "Number of successful measurements for #{name}")
end
def legacy_numerator_counter
prometheus.counter(counter_name('success_total', ':'), "Legacy number of successful measurements for #{name}")
prometheus.counter(counter_name('success_total'), "Number of successful measurements for #{name}")
end
end
@ -117,16 +99,12 @@ module Gitlab
private
def counter_name(suffix, separator)
[COUNTER_PREFIX, name, suffix].join(separator).to_sym
def counter_name(suffix)
[COUNTER_PREFIX, name, suffix].join('_').to_sym
end
def numerator_counter
prometheus.counter(counter_name('error_total', '_'), "Number of error measurements for #{name}")
end
def legacy_numerator_counter
prometheus.counter(counter_name('error_total', ':'), "Number of error measurements for #{name}")
prometheus.counter(counter_name('error_total'), "Number of error measurements for #{name}")
end
end
end

View File

@ -10086,6 +10086,9 @@ msgstr ""
msgid "ContainerRegistry|Set up cleanup"
msgstr ""
msgid "ContainerRegistry|Show full path"
msgstr ""
msgid "ContainerRegistry|Some tags were not deleted"
msgstr ""

View File

@ -1,6 +1,7 @@
import { GlIcon, GlSprintf, GlSkeletonLoader } from '@gitlab/ui';
import { GlIcon, GlSprintf, GlSkeletonLoader, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { mockTracking } from 'helpers/tracking_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import DeleteButton from '~/packages_and_registries/container_registry/explorer/components/delete_button.vue';
import CleanupStatus from '~/packages_and_registries/container_registry/explorer/components/list_page/cleanup_status.vue';
@ -30,13 +31,15 @@ describe('Image List Row', () => {
const findCleanupStatus = () => wrapper.findComponent(CleanupStatus);
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findListItemComponent = () => wrapper.findComponent(ListItem);
const findShowFullPathButton = () => wrapper.findComponent(GlButton);
const mountComponent = (props) => {
const mountComponent = (props, features = {}) => {
wrapper = shallowMount(Component, {
stubs: {
RouterLink,
GlSprintf,
ListItem,
GlButton,
},
propsData: {
item,
@ -44,6 +47,9 @@ describe('Image List Row', () => {
},
provide: {
config: {},
glFeatures: {
...features,
},
},
directives: {
GlTooltip: createMockDirective(),
@ -95,7 +101,7 @@ describe('Image List Row', () => {
});
});
it(`when the image has no name lists the path`, () => {
it('when the image has no name lists the path', () => {
mountComponent({ item: { ...item, name: '' } });
expect(findDetailsLink().text()).toBe(item.path);
@ -143,6 +149,35 @@ describe('Image List Row', () => {
expect(findClipboardButton().attributes('disabled')).toBe('true');
});
});
describe('when containerRegistryShowShortenedPath feature enabled', () => {
let trackingSpy;
beforeEach(() => {
mountComponent({}, { containerRegistryShowShortenedPath: true });
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
it('renders shortened name of image', () => {
expect(findShowFullPathButton().exists()).toBe(true);
expect(findDetailsLink().text()).toBe('gitlab-test/rails-12009');
});
it('clicking on shortened name of image hides the button & shows full path', async () => {
const btn = findShowFullPathButton();
const mockFocusFn = jest.fn();
wrapper.vm.$refs.imageName.$el.focus = mockFocusFn;
await btn.trigger('click');
expect(findShowFullPathButton().exists()).toBe(false);
expect(findDetailsLink().text()).toBe(item.path);
expect(mockFocusFn).toHaveBeenCalled();
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_show_full_path', {
label: 'registry_image_list',
});
});
});
});
describe('delete button', () => {

View File

@ -11,6 +11,10 @@ export const imagesListResponse = [
createdAt: '2020-11-03T13:29:21Z',
expirationPolicyStartedAt: null,
expirationPolicyCleanupStatus: 'UNSCHEDULED',
project: {
id: 'gid://gitlab/Project/22',
path: 'gitlab-test',
},
},
{
__typename: 'ContainerRepository',
@ -24,6 +28,10 @@ export const imagesListResponse = [
createdAt: '2020-09-21T06:57:43Z',
expirationPolicyStartedAt: null,
expirationPolicyCleanupStatus: 'UNSCHEDULED',
project: {
id: 'gid://gitlab/Project/22',
path: 'gitlab-test',
},
},
];

View File

@ -72,14 +72,23 @@ describe('Test report extension', () => {
});
describe('summary', () => {
it('displays loading text', () => {
it('displays loading state initially', () => {
mockApi(httpStatusCodes.OK);
createComponent();
expect(wrapper.text()).toContain(i18n.loading);
});
it('displays failed loading text', async () => {
it('with a 204 response, continues to display loading state', async () => {
mockApi(httpStatusCodes.NO_CONTENT, '');
createComponent();
await waitForPromises();
expect(wrapper.text()).toContain(i18n.loading);
});
it('with an error response, displays failed to load text', async () => {
mockApi(httpStatusCodes.INTERNAL_SERVER_ERROR);
createComponent();

View File

@ -0,0 +1,242 @@
import * as utils from '~/vue_merge_request_widget/extensions/test_report/utils';
describe('test report widget extension utils', () => {
describe('summaryTextbuilder', () => {
it('should render text for no changed results in multiple tests', () => {
const name = 'Test summary';
const data = { total: 10 };
const result = utils.summaryTextBuilder(name, data);
expect(result).toBe(
'Test summary: %{strong_start}no%{strong_end} changed test results, %{strong_start}10%{strong_end} total tests',
);
});
it('should render text for no changed results in one test', () => {
const name = 'Test summary';
const data = { total: 1 };
const result = utils.summaryTextBuilder(name, data);
expect(result).toBe(
'Test summary: %{strong_start}no%{strong_end} changed test results, %{strong_start}1%{strong_end} total test',
);
});
it('should render text for multiple failed results', () => {
const name = 'Test summary';
const data = { failed: 3, total: 10 };
const result = utils.summaryTextBuilder(name, data);
expect(result).toBe(
'Test summary: %{strong_start}3%{strong_end} failed, %{strong_start}10%{strong_end} total tests',
);
});
it('should render text for multiple errored results', () => {
const name = 'Test summary';
const data = { errored: 7, total: 10 };
const result = utils.summaryTextBuilder(name, data);
expect(result).toBe(
'Test summary: %{strong_start}7%{strong_end} errors, %{strong_start}10%{strong_end} total tests',
);
});
it('should render text for multiple fixed results', () => {
const name = 'Test summary';
const data = { resolved: 4, total: 10 };
const result = utils.summaryTextBuilder(name, data);
expect(result).toBe(
'Test summary: %{strong_start}4%{strong_end} fixed test results, %{strong_start}10%{strong_end} total tests',
);
});
it('should render text for multiple fixed, and multiple failed results', () => {
const name = 'Test summary';
const data = { failed: 3, resolved: 4, total: 10 };
const result = utils.summaryTextBuilder(name, data);
expect(result).toBe(
'Test summary: %{strong_start}3%{strong_end} failed and %{strong_start}4%{strong_end} fixed test results, %{strong_start}10%{strong_end} total tests',
);
});
it('should render text for a singular fixed, and a singular failed result', () => {
const name = 'Test summary';
const data = { failed: 1, resolved: 1, total: 10 };
const result = utils.summaryTextBuilder(name, data);
expect(result).toBe(
'Test summary: %{strong_start}1%{strong_end} failed and %{strong_start}1%{strong_end} fixed test result, %{strong_start}10%{strong_end} total tests',
);
});
it('should render text for singular failed, errored, and fixed results', () => {
const name = 'Test summary';
const data = { failed: 1, errored: 1, resolved: 1, total: 10 };
const result = utils.summaryTextBuilder(name, data);
expect(result).toBe(
'Test summary: %{strong_start}1%{strong_end} failed, %{strong_start}1%{strong_end} error and %{strong_start}1%{strong_end} fixed test result, %{strong_start}10%{strong_end} total tests',
);
});
it('should render text for multiple failed, errored, and fixed results', () => {
const name = 'Test summary';
const data = { failed: 2, errored: 3, resolved: 4, total: 10 };
const result = utils.summaryTextBuilder(name, data);
expect(result).toBe(
'Test summary: %{strong_start}2%{strong_end} failed, %{strong_start}3%{strong_end} errors and %{strong_start}4%{strong_end} fixed test results, %{strong_start}10%{strong_end} total tests',
);
});
});
describe('reportTextBuilder', () => {
const name = 'Rspec';
it('should render text for no changed results in multiple tests', () => {
const data = { name, summary: { total: 10 } };
const result = utils.reportTextBuilder(data);
expect(result).toBe('Rspec: no changed test results, 10 total tests');
});
it('should render text for no changed results in one test', () => {
const data = { name, summary: { total: 1 } };
const result = utils.reportTextBuilder(data);
expect(result).toBe('Rspec: no changed test results, 1 total test');
});
it('should render text for multiple failed results', () => {
const data = { name, summary: { failed: 3, total: 10 } };
const result = utils.reportTextBuilder(data);
expect(result).toBe('Rspec: 3 failed, 10 total tests');
});
it('should render text for multiple errored results', () => {
const data = { name, summary: { errored: 7, total: 10 } };
const result = utils.reportTextBuilder(data);
expect(result).toBe('Rspec: 7 errors, 10 total tests');
});
it('should render text for multiple fixed results', () => {
const data = { name, summary: { resolved: 4, total: 10 } };
const result = utils.reportTextBuilder(data);
expect(result).toBe('Rspec: 4 fixed test results, 10 total tests');
});
it('should render text for multiple fixed, and multiple failed results', () => {
const data = { name, summary: { failed: 3, resolved: 4, total: 10 } };
const result = utils.reportTextBuilder(data);
expect(result).toBe('Rspec: 3 failed and 4 fixed test results, 10 total tests');
});
it('should render text for a singular fixed, and a singular failed result', () => {
const data = { name, summary: { failed: 1, resolved: 1, total: 10 } };
const result = utils.reportTextBuilder(data);
expect(result).toBe('Rspec: 1 failed and 1 fixed test result, 10 total tests');
});
it('should render text for singular failed, errored, and fixed results', () => {
const data = { name, summary: { failed: 1, errored: 1, resolved: 1, total: 10 } };
const result = utils.reportTextBuilder(data);
expect(result).toBe('Rspec: 1 failed, 1 error and 1 fixed test result, 10 total tests');
});
it('should render text for multiple failed, errored, and fixed results', () => {
const data = { name, summary: { failed: 2, errored: 3, resolved: 4, total: 10 } };
const result = utils.reportTextBuilder(data);
expect(result).toBe('Rspec: 2 failed, 3 errors and 4 fixed test results, 10 total tests');
});
});
describe('recentFailuresTextBuilder', () => {
it.each`
recentlyFailed | failed | expected
${0} | ${1} | ${''}
${1} | ${1} | ${'1 out of 1 failed test has failed more than once in the last 14 days'}
${1} | ${2} | ${'1 out of 2 failed tests has failed more than once in the last 14 days'}
${2} | ${3} | ${'2 out of 3 failed tests have failed more than once in the last 14 days'}
`(
'should render summary for $recentlyFailed out of $failed failures',
({ recentlyFailed, failed, expected }) => {
const result = utils.recentFailuresTextBuilder({ recentlyFailed, failed });
expect(result).toBe(expected);
},
);
});
describe('countRecentlyFailedTests', () => {
it('counts tests with more than one recent failure in a report', () => {
const report = {
new_failures: [{ recent_failures: { count: 2 } }],
existing_failures: [{ recent_failures: { count: 1 } }],
resolved_failures: [{ recent_failures: { count: 20 } }, { recent_failures: { count: 5 } }],
};
const result = utils.countRecentlyFailedTests(report);
expect(result).toBe(3);
});
it('counts tests with more than one recent failure in an array of reports', () => {
const reports = [
{
new_failures: [{ recent_failures: { count: 2 } }],
existing_failures: [
{ recent_failures: { count: 20 } },
{ recent_failures: { count: 5 } },
],
resolved_failures: [{ recent_failures: { count: 2 } }],
},
{
new_failures: [{ recent_failures: { count: 8 } }, { recent_failures: { count: 14 } }],
existing_failures: [{ recent_failures: { count: 1 } }],
resolved_failures: [{ recent_failures: { count: 7 } }, { recent_failures: { count: 5 } }],
},
];
const result = utils.countRecentlyFailedTests(reports);
expect(result).toBe(8);
});
it.each([
[],
{},
null,
undefined,
{ new_failures: undefined },
[{ existing_failures: null }],
{ resolved_failures: [{}] },
[{ new_failures: [{ recent_failures: {} }] }],
])('returns 0 when subject is %s', (subject) => {
const result = utils.countRecentlyFailedTests(subject);
expect(result).toBe(0);
});
});
describe('formatFilePath', () => {
it.each`
file | expected
${'./test.js'} | ${'test.js'}
${'/test.js'} | ${'test.js'}
${'.//////////////test.js'} | ${'test.js'}
${'test.js'} | ${'test.js'}
${'mock/path./test.js'} | ${'mock/path./test.js'}
${'./mock/path./test.js'} | ${'mock/path./test.js'}
`('should format $file to be $expected', ({ file, expected }) => {
expect(utils.formatFilePath(file)).toBe(expected);
});
});
});

View File

@ -1,8 +1,6 @@
# frozen_string_literal: true
# Switch this back to `fast_spec_helper` when removing the `gitlab_sli_new_counters`
# feature flag
require 'spec_helper'
require 'fast_spec_helper'
RSpec.describe Gitlab::Metrics::Sli do
let(:prometheus) { double("prometheus") }
@ -20,16 +18,12 @@ RSpec.describe Gitlab::Metrics::Sli do
it 'allows different SLIs to be defined on each subclass' do
apdex_counters = [
fake_total_counter('foo_apdex'),
fake_numerator_counter('foo_apdex', 'success'),
fake_total_counter('foo_apdex', ':'),
fake_numerator_counter('foo_apdex', 'success', ':')
fake_numerator_counter('foo_apdex', 'success')
]
error_rate_counters = [
fake_total_counter('foo'),
fake_numerator_counter('foo', 'error'),
fake_total_counter('foo', ':'),
fake_numerator_counter('foo', 'error', ':')
fake_numerator_counter('foo', 'error')
]
apdex = described_class::Apdex.initialize_sli(:foo, [{ hello: :world }])
@ -84,9 +78,7 @@ RSpec.describe Gitlab::Metrics::Sli do
it 'returns and stores a new initialized SLI' do
counters = [
fake_total_counter("bar#{subclass_info[:suffix]}"),
fake_numerator_counter("bar#{subclass_info[:suffix]}", subclass_info[:numerator]),
fake_total_counter("bar#{subclass_info[:suffix]}", ':'),
fake_numerator_counter("bar#{subclass_info[:suffix]}", subclass_info[:numerator], ':')
fake_numerator_counter("bar#{subclass_info[:suffix]}", subclass_info[:numerator])
]
sli = described_class.initialize_sli(:bar, [{ hello: :world }])
@ -99,9 +91,7 @@ RSpec.describe Gitlab::Metrics::Sli do
it 'does not change labels for an already-initialized SLI' do
counters = [
fake_total_counter("bar#{subclass_info[:suffix]}"),
fake_numerator_counter("bar#{subclass_info[:suffix]}", subclass_info[:numerator]),
fake_total_counter("bar#{subclass_info[:suffix]}", ':'),
fake_numerator_counter("bar#{subclass_info[:suffix]}", subclass_info[:numerator], ':')
fake_numerator_counter("bar#{subclass_info[:suffix]}", subclass_info[:numerator])
]
sli = described_class.initialize_sli(:bar, [{ hello: :world }])
@ -122,8 +112,6 @@ RSpec.describe Gitlab::Metrics::Sli do
before do
fake_total_counter("boom#{subclass_info[:suffix]}")
fake_numerator_counter("boom#{subclass_info[:suffix]}", subclass_info[:numerator])
fake_total_counter("boom#{subclass_info[:suffix]}", ':')
fake_numerator_counter("boom#{subclass_info[:suffix]}", subclass_info[:numerator], ':')
end
it 'is true when an SLI was initialized with labels' do
@ -142,9 +130,7 @@ RSpec.describe Gitlab::Metrics::Sli do
it 'initializes counters for the passed label combinations' do
counters = [
fake_total_counter("hey#{subclass_info[:suffix]}"),
fake_numerator_counter("hey#{subclass_info[:suffix]}", subclass_info[:numerator]),
fake_total_counter("hey#{subclass_info[:suffix]}", ':'),
fake_numerator_counter("hey#{subclass_info[:suffix]}", subclass_info[:numerator], ':')
fake_numerator_counter("hey#{subclass_info[:suffix]}", subclass_info[:numerator])
]
described_class.new(:hey).initialize_counters([{ foo: 'bar' }, { foo: 'baz' }])
@ -152,43 +138,18 @@ RSpec.describe Gitlab::Metrics::Sli do
expect(counters).to all(have_received(:get).with({ foo: 'bar' }))
expect(counters).to all(have_received(:get).with({ foo: 'baz' }))
end
context 'when `gitlab_sli_new_counters` is disabled' do
before do
stub_feature_flags(gitlab_sli_new_counters: false)
end
it 'does not initialize the new counters', :aggregate_failures do
new_total_counter = fake_total_counter("bar#{subclass_info[:suffix]}")
new_numerator_counter = fake_numerator_counter("bar#{subclass_info[:suffix]}", subclass_info[:numerator])
fake_total_counter("bar#{subclass_info[:suffix]}", ':')
fake_numerator_counter("bar#{subclass_info[:suffix]}", subclass_info[:numerator], ':')
described_class.new(:bar).initialize_counters([{ hello: :world }])
expect(new_total_counter).not_to have_received(:get)
expect(new_numerator_counter).not_to have_received(:get)
end
end
end
describe "#increment" do
let!(:sli) { described_class.new(:heyo) }
let!(:total_counter) { fake_total_counter("heyo#{subclass_info[:suffix]}") }
let!(:numerator_counter) { fake_numerator_counter("heyo#{subclass_info[:suffix]}", subclass_info[:numerator]) }
let!(:legacy_total_counter) { fake_total_counter("heyo#{subclass_info[:suffix]}", ':') }
let!(:legacy_numerator_counter) do
fake_numerator_counter("heyo#{subclass_info[:suffix]}", subclass_info[:numerator], ':')
end
it "increments both counters for labels when #{subclass_info[:numerator]} is true" do
sli.increment(labels: { hello: "world" }, subclass_info[:numerator] => true)
expect(total_counter).to have_received(:increment).with({ hello: 'world' })
expect(numerator_counter).to have_received(:increment).with({ hello: 'world' })
expect(legacy_total_counter).to have_received(:increment).with({ hello: 'world' })
expect(legacy_numerator_counter).to have_received(:increment).with({ hello: 'world' })
end
it "only increments the total counters for labels when #{subclass_info[:numerator]} is false" do
@ -196,31 +157,6 @@ RSpec.describe Gitlab::Metrics::Sli do
expect(total_counter).to have_received(:increment).with({ hello: 'world' })
expect(numerator_counter).not_to have_received(:increment).with({ hello: 'world' })
expect(legacy_total_counter).to have_received(:increment).with({ hello: 'world' })
expect(legacy_numerator_counter).not_to have_received(:increment).with({ hello: 'world' })
end
context 'when `gitlab_sli_new_counters` is disabled' do
before do
stub_feature_flags(gitlab_sli_new_counters: false)
end
it 'does increment new counters', :aggregate_failures do
new_total_counter = fake_total_counter("bar#{subclass_info[:suffix]}")
new_numerator_counter = fake_numerator_counter("bar#{subclass_info[:suffix]}", subclass_info[:numerator])
legacy_total_counter = fake_total_counter("bar#{subclass_info[:suffix]}", ':')
legacy_numerator_counter = fake_numerator_counter(
"bar#{subclass_info[:suffix]}", subclass_info[:numerator], ':'
)
described_class.new(:bar).increment(labels: { hello: 'world' }, subclass_info[:numerator] => true)
expect(new_total_counter).not_to have_received(:increment)
expect(new_numerator_counter).not_to have_received(:increment)
expect(legacy_total_counter).to have_received(:increment)
expect(legacy_numerator_counter).to have_received(:increment)
end
end
end
end