Commit 26f4a225 authored by GitLab Bot's avatar GitLab Bot

Automatic merge of gitlab-org/gitlab master

parents 49e49cfb 8ad16eb9
......@@ -73,6 +73,7 @@ eslint-report.html
/.gitlab_pages_secret
/.gitlab_kas_secret
/webpack-report/
/crystalball/
/knapsack/
/rspec_flaky/
/locale/**/LC_MESSAGES
......
......@@ -59,6 +59,8 @@ variables:
GET_SOURCES_ATTEMPTS: "3"
KNAPSACK_RSPEC_SUITE_REPORT_PATH: knapsack/report-master.json
FLAKY_RSPEC_SUITE_REPORT_PATH: rspec_flaky/report-suite.json
RSPEC_TESTS_MAPPING_PATH: crystalball/mapping.json
RSPEC_PACKED_TESTS_MAPPING_PATH: crystalball/packed-mapping.json
BUILD_ASSETS_IMAGE: "false"
ES_JAVA_OPTS: "-Xms256m -Xmx256m"
ELASTIC_URL: "http://elastic:changeme@elasticsearch:9200"
......
......@@ -20,6 +20,7 @@
variables:
RUBY_GC_MALLOC_LIMIT: 67108864
RUBY_GC_MALLOC_LIMIT_MAX: 134217728
CRYSTALBALL: "true"
needs: ["setup-test-env", "retrieve-tests-metadata", "compile-test-assets"]
script:
- *base-script
......@@ -29,6 +30,7 @@
when: always
paths:
- coverage/
- crystalball/
- knapsack/
- rspec_flaky/
- rspec_profiling/
......
......@@ -9,6 +9,7 @@
- knapsack/
- rspec_flaky/
- rspec_profiling/
- crystalball/
retrieve-tests-metadata:
extends:
......@@ -41,3 +42,4 @@ update-tests-metadata:
- run_timed_command "retry gem install bundler:1.17.3 fog-aws mime-types activesupport rspec_profiling postgres-copy --no-document"
- source ./scripts/rspec_helpers.sh
- update_tests_metadata
- update_tests_mapping
322d3608e68f6ed1dc2efa9164f3026006f43e12
cf8e99ccc104f0a43f41e54896ee46a5e1b15a0a
......@@ -386,6 +386,7 @@ group :development, :test do
gem 'benchmark-ips', '~> 2.3.0', require: false
gem 'knapsack', '~> 1.17'
gem 'crystalball', '~> 0.7.0', require: false
gem 'simple_po_parser', '~> 1.1.2', require: false
......
......@@ -199,6 +199,8 @@ GEM
safe_yaml (~> 1.0.0)
crass (1.0.6)
creole (0.5.0)
crystalball (0.7.0)
git
css_parser (1.7.0)
addressable
daemons (1.2.6)
......@@ -1291,6 +1293,7 @@ DEPENDENCIES
connection_pool (~> 2.0)
countries (~> 3.0)
creole (~> 0.5.0)
crystalball (~> 0.7.0)
danger (~> 8.0.6)
database_cleaner (~> 1.7.0)
deckar01-task_list (= 2.3.1)
......
<script>
import DevopsAdoptionEmptyState from './devops_adoption_empty_state.vue';
export default {
name: 'DevopsAdoptionApp',
components: {
DevopsAdoptionEmptyState,
},
};
</script>
<template>
<devops-adoption-empty-state />
</template>
<script>
import { GlEmptyState, GlButton } from '@gitlab/ui';
import { DEVOPS_ADOPTION_STRINGS } from '../constants';
export default {
name: 'DevopsAdoptionEmptyState',
inject: ['emptyStateSvgPath'],
components: {
GlEmptyState,
GlButton,
},
i18n: DEVOPS_ADOPTION_STRINGS.emptyState,
};
</script>
<template>
<gl-empty-state
:title="$options.i18n.title"
:description="$options.i18n.description"
:svg-path="emptyStateSvgPath"
>
<template #actions>
<gl-button variant="info">{{ $options.i18n.button }}</gl-button>
</template>
</gl-empty-state>
</template>
import { s__ } from '~/locale';
export const DEVOPS_ADOPTION_STRINGS = {
emptyState: {
title: s__('DevopsAdoption|Add a segment to get started'),
description: s__(
'DevopsAdoption|DevOps adoption uses segments to track adoption across key features. Segments are a way to track multiple related projects and groups at once. For example, you could create a segment for the engineering department or a particular product team.',
),
button: s__('DevopsAdoption|Add new segment'),
},
};
import Vue from 'vue';
import DevopsAdoptionApp from './components/devops_adoption_app.vue';
export default () => {
const el = document.querySelector('.js-devops-adoption');
if (!el) return false;
const { emptyStateSvgPath } = el.dataset;
return new Vue({
el,
provide: {
emptyStateSvgPath,
},
render(h) {
return h(DevopsAdoptionApp);
},
});
};
import Vue from 'vue';
import UserCallout from '~/user_callout';
import UsagePingDisabled from './components/usage_ping_disabled.vue';
export default () => {
// eslint-disable-next-line no-new
new UserCallout();
const emptyStateContainer = document.getElementById('js-devops-empty-state');
if (!emptyStateContainer) return false;
const { emptyStateSvgPath, enableUsagePingLink, docsLink, isAdmin } = emptyStateContainer.dataset;
return new Vue({
el: emptyStateContainer,
provide: {
isAdmin: Boolean(isAdmin),
svgPath: emptyStateSvgPath,
primaryButtonPath: enableUsagePingLink,
docsLink,
},
render(h) {
return h(UsagePingDisabled);
},
});
};
import Vue from 'vue';
import UserCallout from '~/user_callout';
import UsagePingDisabled from '~/admin/dev_ops_report/components/usage_ping_disabled.vue';
import initDevOpsScoreEmptyState from '~/admin/dev_ops_report/devops_score_empty_state';
import initDevopAdoption from '~/admin/dev_ops_report/devops_adoption';
document.addEventListener('DOMContentLoaded', () => {
// eslint-disable-next-line no-new
new UserCallout();
const emptyStateContainer = document.getElementById('js-devops-empty-state');
if (!emptyStateContainer) return false;
const { emptyStateSvgPath, enableUsagePingLink, docsLink, isAdmin } = emptyStateContainer.dataset;
return new Vue({
el: emptyStateContainer,
provide: {
isAdmin: Boolean(isAdmin),
svgPath: emptyStateSvgPath,
primaryButtonPath: enableUsagePingLink,
docsLink,
},
render(h) {
return h(UsagePingDisabled);
},
});
});
initDevOpsScoreEmptyState();
initDevopAdoption();
......@@ -174,6 +174,10 @@ module GroupsHelper
!multiple_members?(group)
end
def show_thanks_for_purchase_banner?
params.key?(:purchased_quantity) && params[:purchased_quantity].to_i > 0
end
private
def just_created?
......
......@@ -103,6 +103,7 @@ class BroadcastMessage < ApplicationRecord
end
def matches_current_path(current_path)
return false if current_path.blank? && target_path.present?
return true if current_path.blank? || target_path.blank?
escaped = Regexp.escape(target_path).gsub('\\*', '.*')
......
......@@ -14,7 +14,7 @@
.tab-pane.active#devops_score_pane
= render 'report'
.tab-pane#devops_adoption_pane
.js-devops-adoption
.js-devops-adoption{ data: { empty_state_svg_path: image_path('illustrations/monitoring/getting_started.svg') } }
- else
= render 'report'
- breadcrumb_title _("Details")
- @content_class = "limit-container-width" unless fluid_layout
- if show_thanks_for_purchase_banner?
= render_if_exists 'shared/thanks_for_purchase_banner', plan_title: plan_title, quantity: params[:purchased_quantity].to_i
- if show_invite_banner?(@group)
= content_for :group_invite_members_banner do
.container-fluid.container-limited{ class: "gl-pb-2! gl-pt-6! #{@content_class}" }
......
---
title: Disallow WebIDE route in robots.txt
merge_request: 46117
author:
type: changed
---
title: Don't return target-specific broadcasts without a current path supplied
merge_request: 46322
author:
type: fixed
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# API Docs
Automate GitLab via a simple and powerful API.
......
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Group-level Variables API
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/34519) in GitLab 9.5
......
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Groups API
## List groups
......
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Import API
## Import repository from GitHub
......
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Instance clusters API
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/36001) in GitLab 13.2.
......
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Issue links API **(CORE)**
> The simple "relates to" relationship [moved](https://gitlab.com/gitlab-org/gitlab/-/issues/212329) to [GitLab Core](https://about.gitlab.com/pricing/) in 13.4.
......
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Issues Statistics API
Every API call to issues_statistics must be authenticated.
......
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Job Artifacts API
## Get job artifacts
......
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# License **(CORE ONLY)**
To interact with license endpoints, you need to authenticate yourself as an
......
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Managed Licenses API **(ULTIMATE)**
## List managed licenses
......
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Group and project members API
## Valid access levels
......
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Namespaces API
Usernames and groupnames fall under a special category called namespaces.
......
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Notes API
Notes are comments on:
......
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Notification settings API
> [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/merge_requests/5632) in GitLab 8.12.
......
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Personal access tokens API **(ULTIMATE)**
You can read more about [personal access tokens](../user/profile/personal_access_tokens.md#personal-access-tokens).
......
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Resource label events API
Resource label events keep track about who, when, and which label was added to, or removed from, an issuable.
......
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# SCIM API **(SILVER ONLY)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/9388) in [GitLab Silver](https://about.gitlab.com/pricing/) 11.10.
......
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Services API
NOTE: **Note:**
......
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Application settings API **(CORE ONLY)**
These API calls allow you to read and modify GitLab instance
......
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Sidekiq Metrics API **(CORE ONLY)**
> Introduced in GitLab 8.9.
......
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Application statistics API
## Get current application statistics
......
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# System hooks API
All methods require administrator authorization.
......
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
type: reference
---
......
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
type: reference
---
......
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
type: reference
---
......
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Users API
## List users
......
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# API V3 to API V4
In GitLab 9.0 and later, API V4 is the preferred version to be used.
......
---
stage: none
group: unassigned
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers
---
# Version API
> Introduced in GitLab 8.13.
......
......@@ -188,6 +188,30 @@ if Feature.disabled?(:my_feature_flag, project, type: :ops)
end
```
DANGER: **Warning:**
Don't use feature flags at application load time. For example, using the `Feature` class in
`config/initializers/*` or at the class level could cause an unexpected error. This error occurs
because a database that a feature flag adapter might depend on doesn't exist at load time
(especially for fresh installations). Checking for the database's existence at the caller isn't
recommended, as some adapters don't require a database at all (for example, the HTTP adapter). The
feature flag setup check must be abstracted in the `Feature` namespace. This approach also requires
application reload when the feature flag changes. You must therefore ask SREs to reload the
Web/API/Sidekiq fleet on production, which takes time to fully rollout/rollback the changes. For
these reasons, use environment variables (for example, `ENV['YOUR_FEATURE_NAME']`) or `gitlab.yml`
instead.
Here's an example of a pattern that you should avoid:
```ruby
class MyClass
if Feature.enabled?(:...)
new_process
else
legacy_process
end
end
```
### Frontend
Use the `push_frontend_feature_flag` method for frontend code, which is
......
......@@ -60,10 +60,10 @@ class SubscriptionsController < ApplicationController
).execute
if response[:success]
plan_id, quantity = subscription_params.values_at(:plan_id, :quantity)
redirect_location = if params[:selected_group]
group_path(group)
group_path(group, plan_id: plan_id, purchased_quantity: quantity)
else
plan_id, quantity = subscription_params.values_at(:plan_id, :quantity)
edit_subscriptions_group_path(group.path, plan_id: plan_id, quantity: quantity, new_user: params[:new_user])
end
......
%section.gl-banner.gl-banner-introduction.gl-p-3.px-lg-6
.gl-banner-illustration.gl-display-flex
= image_tag('illustrations/illustration-congratulation-purchase.svg', class: 'mw-xs')
.gl-banner-content.gl-display-flex.gl-flex-direction-column.gl-justify-content-center
%h3= _('Thanks for your purchase!')
- number_of_users = n_('1 user', '%{num} users', quantity) % { num: quantity }
%p= _('You have successfully purchased a %{plan} plan subscription for %{seats}. You’ll receive a receipt via email.') % { plan: plan_title, seats: number_of_users }
......@@ -2,13 +2,7 @@
.row.flex-grow-1.bg-gray-light
.d-flex.flex-column.align-items-center.w-100.gl-p-5
%section.gl-banner.gl-banner-introduction.gl-p-3.px-lg-6
.gl-banner-illustration.d-flex
= image_tag('illustrations/subscription-success.svg', class: 'mw-xs')
.gl-banner-content.d-flex.flex-column.justify-content-center
%h3= _('Thanks for your purchase!')
- number_of_users = n_('1 user', '%{num} users', params[:quantity].to_i) % { num: params[:quantity] }
%p= _('You have successfully purchased a %{plan} plan subscription for %{seats}. You’ll receive a receipt via email.') % { plan: plan_title, seats: number_of_users }
= render 'shared/thanks_for_purchase_banner', plan_title: plan_title, quantity: params[:quantity].to_i
.edit-group.d-flex.flex-column.align-items-center.gl-pt-7
- if params[:new_user]
#progress-bar
......
---
title: Show purchase confirmation banner when user makes new purchases for existing
groups
merge_request: 45786
author:
type: fixed
......@@ -211,7 +211,10 @@ RSpec.describe SubscriptionsController do
it 'returns the selected group location in JSON format' do
subject
expect(response.body).to eq({ location: "/#{selected_group.path}" }.to_json)
plan_id = params[:subscription][:plan_id]
quantity = params[:subscription][:quantity]
expect(response.body).to eq({ location: "/#{selected_group.path}?plan_id=#{plan_id}&purchased_quantity=#{quantity}" }.to_json)
end
context 'when selected group is a sub group' do
......
......@@ -348,7 +348,7 @@ RSpec.describe 'Customizable Group Value Stream Analytics', :js do
expect(page).to have_text custom_stage_name
end
it_behaves_like 'can edit custom stages'
it_behaves_like 'can edit custom stages', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/273045'
end
end
......
......@@ -9223,6 +9223,15 @@ msgstr ""
msgid "DevOps Score"
msgstr ""
msgid "DevopsAdoption|Add a segment to get started"
msgstr ""
msgid "DevopsAdoption|Add new segment"
msgstr ""
msgid "DevopsAdoption|DevOps adoption uses segments to track adoption across key features. Segments are a way to track multiple related projects and groups at once. For example, you could create a segment for the engineering department or a particular product team."
msgstr ""
msgid "Diff content limits"
msgstr ""
......
......@@ -23,6 +23,7 @@ Disallow: /users
Disallow: /help
Disallow: /s/
Disallow: /-/profile
Disallow: /-/ide/
# Only specifically allow the Sign In page to avoid very ugly search results
Allow: /users/sign_in
......
#!/usr/bin/env ruby
require 'json'
require_relative '../tooling/lib/tooling/test_map_generator'
test_mapping_json = ARGV.shift
crystalball_yamls = ARGV
unless test_mapping_json && !crystalball_yamls.empty?
puts "usage: #{__FILE__} <test_mapping_json> [crystalball_yamls...]"
exit 1
end
map_generator = Tooling::TestMapGenerator.new
map_generator.parse(crystalball_yamls)
mapping = map_generator.mapping
File.write(test_mapping_json, JSON.pretty_generate(mapping))
puts "Saved #{test_mapping_json}."
#!/usr/bin/env ruby
require 'json'
require_relative '../tooling/lib/tooling/test_map_packer'
unpacked_json_mapping, packed_json_mapping = ARGV.shift(2)
unless packed_json_mapping && unpacked_json_mapping
puts "usage: #{__FILE__} <unpacked_json_mapping> <packed_json_mapping>"
exit 1
end
puts "Compressing #{unpacked_json_mapping}"
mapping = JSON.parse(File.read(unpacked_json_mapping))
packed_mapping = Tooling::TestMapPacker.new.pack(mapping)
puts "Writing packed #{packed_json_mapping}"
File.write(packed_json_mapping, JSON.generate(packed_mapping))
puts "Saved #{packed_json_mapping}."
......@@ -48,6 +48,43 @@ function update_tests_metadata() {
fi
}
function retrieve_tests_mapping() {
mkdir -p crystalball/
if [[ ! -f "${RSPEC_PACKED_TESTS_MAPPING_PATH}" ]]; then
(wget -O "${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz" "http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz" && gzip -d "${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz") || echo "{}" > "${RSPEC_PACKED_TESTS_MAPPING_PATH}"
fi
scripts/unpack-test-mapping "${RSPEC_PACKED_TESTS_MAPPING_PATH}" "${RSPEC_TESTS_MAPPING_PATH}"
}
function update_tests_mapping() {
if ! crystalball_rspec_data_exists; then
echo "No crystalball rspec data found."
return 0
fi
scripts/generate-test-mapping "${RSPEC_TESTS_MAPPING_PATH}" crystalball/rspec*.yml
scripts/pack-test-mapping "${RSPEC_TESTS_MAPPING_PATH}" "${RSPEC_PACKED_TESTS_MAPPING_PATH}"
gzip "${RSPEC_PACKED_TESTS_MAPPING_PATH}"
if [[ -n "${TESTS_METADATA_S3_BUCKET}" ]]; then
if [[ "$CI_PIPELINE_SOURCE" == "schedule" ]]; then
scripts/sync-reports put "${TESTS_METADATA_S3_BUCKET}" "${RSPEC_PACKED_TESTS_MAPPING_PATH}.gz"
else
echo "Not uploading report to S3 as the pipeline is not a scheduled one."
fi
fi
rm -f crystalball/rspec*.yml
}
function crystalball_rspec_data_exists() {
compgen -G "crystalball/rspec*.yml" > /dev/null;
}
function rspec_simple_job() {
local rspec_opts="${1}"
......
#!/usr/bin/env ruby
require 'json'
require_relative '../tooling/lib/tooling/test_map_packer'
packed_json_mapping, unpacked_json_mapping = ARGV.shift(2)
unless packed_json_mapping && unpacked_json_mapping
puts "usage: #{__FILE__} <packed_json_mapping> <unpacked_json_mapping>"
exit 1
end
packed_mapping = JSON.parse(File.read(packed_json_mapping))
mapping = Tooling::TestMapPacker.new.unpack(packed_mapping)
puts "Writing unpacked #{unpacked_json_mapping}"
File.write(unpacked_json_mapping, JSON.generate(mapping))
puts "Saved #{unpacked_json_mapping}."
# frozen_string_literal: true
module CrystalballEnv
EXCLUDED_PREFIXES = %w[vendor/ruby].freeze
extend self
def start!
return unless ENV['CRYSTALBALL'] && ENV['CI_PIPELINE_SOURCE'] == 'schedule'
require 'crystalball'
require_relative '../tooling/lib/tooling/crystalball/coverage_lines_execution_detector'
require_relative '../tooling/lib/tooling/crystalball/coverage_lines_strategy'
map_storage_path_base = ENV['CI_JOB_NAME'] || 'crystalball_data'
map_storage_path = "crystalball/#{map_storage_path_base.gsub(%r{[/ ]}, '_')}.yml"
execution_detector = Tooling::Crystalball::CoverageLinesExecutionDetector.new(exclude_prefixes: EXCLUDED_PREFIXES)
Crystalball::MapGenerator.start! do |config|
config.map_storage_path = map_storage_path
config.register Tooling::Crystalball::CoverageLinesStrategy.new(execution_detector)
end
end
end
import { shallowMount } from '@vue/test-utils';
import DevopsAdoptionApp from '~/admin/dev_ops_report/components/devops_adoption_app.vue';
import DevopsAdoptionEmptyState from '~/admin/dev_ops_report/components/devops_adoption_empty_state.vue';
describe('DevopsAdoptionApp', () => {
let wrapper;
const createComponent = () => {
return shallowMount(DevopsAdoptionApp);
};
beforeEach(() => {
wrapper = createComponent();
});
describe('default behaviour', () => {
it('displays the empty state', () => {
expect(wrapper.find(DevopsAdoptionEmptyState).exists()).toBe(true);
});
});
});
import { shallowMount } from '@vue/test-utils';
import { GlEmptyState, GlButton } from '@gitlab/ui';
import DevopsAdoptionEmptyState from '~/admin/dev_ops_report/components/devops_adoption_empty_state.vue';
import { DEVOPS_ADOPTION_STRINGS } from '~/admin/dev_ops_report/constants';
const emptyStateSvgPath = 'illustrations/monitoring/getting_started.svg';
describe('DevopsAdoptionEmptyState', () => {
let wrapper;
const createComponent = (options = {}) => {
const { stubs = {} } = options;
return shallowMount(DevopsAdoptionEmptyState, {
provide: {
emptyStateSvgPath,
},
stubs,
});
};
const findEmptyState = () => wrapper.find(GlEmptyState);
const findEmptyStateAction = () => findEmptyState().find(GlButton);
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('contains the correct svg', () => {
wrapper = createComponent();
expect(findEmptyState().props('svgPath')).toBe(emptyStateSvgPath);
});
it('contains the correct text', () => {
wrapper = createComponent();
const emptyState = findEmptyState();
expect(emptyState.props('title')).toBe(DEVOPS_ADOPTION_STRINGS.emptyState.title);
expect(emptyState.props('description')).toBe(DEVOPS_ADOPTION_STRINGS.emptyState.description);
});
it('contains an overridden action button', () => {
wrapper = createComponent({ stubs: { GlEmptyState } });
const actionButton = findEmptyStateAction();
expect(actionButton.exists()).toBe(true);
expect(actionButton.text()).toBe(DEVOPS_ADOPTION_STRINGS.emptyState.button);
});
});
......@@ -370,6 +370,26 @@ RSpec.describe GroupsHelper do
end
end
describe '#show_thanks_for_purchase_banner?' do
subject { helper.show_thanks_for_purchase_banner? }
it 'returns true with purchased_quantity present in params' do
allow(controller).to receive(:params) { { purchased_quantity: '1' } }
is_expected.to be_truthy
end
it 'returns false with purchased_quantity not present in params' do
is_expected.to be_falsey
end
it 'returns false with purchased_quantity is empty in params' do
allow(controller).to receive(:params) { { purchased_quantity: '' } }
is_expected.to be_falsey
end
end
describe '#show_invite_banner?' do
let_it_be(:current_user) { create(:user) }
let_it_be_with_refind(:group) { create(:group) }
......
......@@ -161,6 +161,12 @@ RSpec.describe BroadcastMessage do
expect(subject.call('/group/issues/test').length).to eq(1)
end
it "does not return message if the target path is set but no current path is provided" do
create(:broadcast_message, target_path: "*/issues/*", broadcast_type: broadcast_type)
expect(subject.call.length).to eq(0)
end
end
describe '.current', :use_clean_rails_memory_store_caching do
......
......@@ -37,6 +37,7 @@ RSpec.describe 'Robots.txt Requests', :aggregate_failures do
'/help',
'/s/',
'/-/profile',
'/-/ide/project',
'/foo/bar/new',
'/foo/bar/edit',
'/foo/bar/raw',
......
......@@ -3,6 +3,9 @@
require './spec/simplecov_env'
SimpleCovEnv.start!
require './spec/crystalball_env'
CrystalballEnv.start!
ENV["RAILS_ENV"] = 'test'
ENV["IN_MEMORY_APPLICATION_SETTINGS"] = 'true'
ENV["RSPEC_ALLOW_INVALID_URLS"] = 'true'
......
# frozen_string_literal: true
require_relative '../../../../../tooling/lib/tooling/crystalball/coverage_lines_execution_detector'
RSpec.describe Tooling::Crystalball::CoverageLinesExecutionDetector do
subject(:detector) { described_class.new(root, exclude_prefixes: %w[vendor/ruby]) }
let(:root) { '/tmp' }
let(:before_map) { { path => { lines: [0, 2, nil] } } }
let(:after_map) { { path => { lines: [0, 3, nil] } } }
let(:path) { '/tmp/file.rb' }
describe '#detect' do
subject { detector.detect(before_map, after_map) }
it { is_expected.to eq(%w[file.rb]) }
context 'with no changes' do
let(:after_map) { { path => { lines: [0, 2, nil] } } }
it { is_expected.to eq([]) }
end
context 'with previously uncovered file' do
let(:before_map) { {} }
it { is_expected.to eq(%w[file.rb]) }
end
context 'with path outside of root' do
let(:path) { '/abc/file.rb' }
it { is_expected.to eq([]) }
end
context 'with path in excluded prefix' do
let(:path) { '/tmp/vendor/ruby/dependency.rb' }
it { is_expected.to eq([]) }
end
end
end
# frozen_string_literal: true
require_relative '../../../../../tooling/lib/tooling/crystalball/coverage_lines_strategy'
RSpec.describe Tooling::Crystalball::CoverageLinesStrategy do
subject { described_class.new(execution_detector) }
let(:execution_detector) { instance_double('Tooling::Crystalball::CoverageLinesExecutionDetector') }
describe '#after_register' do
it 'starts coverage' do
expect(Coverage).to receive(:start).with(lines: true)
subject.after_register
end
end
end
# frozen_string_literal: true
require_relative '../../../../tooling/lib/tooling/test_map_generator'
RSpec.describe Tooling::TestMapGenerator do
subject { described_class.new }
describe '#parse' do
let(:yaml1) do
<<~YAML
---
:type: Crystalball::ExecutionMap
:commit: a7d57d333042f3b0334b2f8a282354eef7365976
:timestamp: 1602668405
:version:
---
"./spec/factories_spec.rb[1]":
- lib/gitlab/current_settings.rb
- lib/feature.rb
- lib/gitlab/marginalia.rb
YAML
end
let(:yaml2) do
<<~YAML
---
:type: Crystalball::ExecutionMap
:commit: 74056e8d9cf3773f43faa1cf5416f8779c8284c8
:timestamp: 1602671965
:version:
---
"./spec/models/project_spec.rb[1]":
- lib/gitlab/current_settings.rb
- lib/feature.rb
- lib/gitlab/marginalia.rb
YAML
end
let(:pathname) { instance_double(Pathname) }
before do
allow(File).to receive(:read).with('yaml1.yml').and_return(yaml1)
allow(File).to receive(:read).with('yaml2.yml').and_return(yaml2)
end
context 'with single yaml' do
let(:expected_mapping) do
{
'lib/gitlab/current_settings.rb' => [
'./spec/factories_spec.rb'
],
'lib/feature.rb' => [
'./spec/factories_spec.rb'
],
'lib/gitlab/marginalia.rb' => [
'./spec/factories_spec.rb'
]
}
end
it 'parses crystalball data into test mapping' do
subject.parse('yaml1.yml')
expect(subject.mapping.keys).to match_array(expected_mapping.keys)
end
it 'stores test files without example uid' do
subject.parse('yaml1.yml')
expected_mapping.each do |file, tests|
expect(subject.mapping[file]).to match_array(tests)
end
end
end
context 'with multiple yamls' do
let(:expected_mapping) do
{
'lib/gitlab/current_settings.rb' => [
'./spec/factories_spec.rb',
'./spec/models/project_spec.rb'
],
'lib/feature.rb' => [
'./spec/factories_spec.rb',
'./spec/models/project_spec.rb'
],
'lib/gitlab/marginalia.rb' => [
'./spec/factories_spec.rb',
'./spec/models/project_spec.rb'
]
}
end
it 'parses crystalball data into test mapping' do
subject.parse(%w[yaml1.yml yaml2.yml])
expect(subject.mapping.keys).to match_array(expected_mapping.keys)
end
it 'stores test files without example uid' do
subject.parse(%w[yaml1.yml yaml2.yml])
expected_mapping.each do |file, tests|
expect(subject.mapping[file]).to match_array(tests)
end
end
end
end
end
# frozen_string_literal: true
require_relative '../../../../tooling/lib/tooling/test_map_packer'
RSpec.describe Tooling::TestMapPacker do
subject { described_class.new }
let(:map) do
{
'file1.rb' => [
'./a/b/c/test_1.rb',
'./a/b/test_2.rb',
'./a/b/test_3.rb',
'./a/test_4.rb',
'./test_5.rb'
],
'file2.rb' => [
'./a/b/c/test_1.rb',
'./a/test_4.rb',
'./test_5.rb'
]
}
end
let(:compact_map) do
{
'file1.rb' => {
'.' => {
'a' => {
'b' => {
'c' => {
'test_1.rb' => 1
},
'test_2.rb' => 1,
'test_3.rb' => 1
},
'test_4.rb' => 1
},
'test_5.rb' => 1
}
},
'file2.rb' => {
'.' => {
'a' => {
'b' => {
'c' => {
'test_1.rb' => 1
}
},
'test_4.rb' => 1
},
'test_5.rb' => 1
}
}
}
end
describe '#pack' do
it 'compacts list of test files into a prefix tree' do
expect(subject.pack(map)).to eq(compact_map)
end
it 'does nothing to empty hash' do
expect(subject.pack({})).to eq({})
end
end
describe '#unpack' do
it 'unpack prefix tree into list of test files' do
expect(subject.unpack(compact_map)).to eq(map)
end
it 'does nothing to empty hash' do
expect(subject.unpack({})).to eq({})
end
end
end
# frozen_string_literal: true
require 'crystalball/map_generator/helpers/path_filter'
module Tooling
module Crystalball
# Class for detecting code execution path based on coverage information diff
class CoverageLinesExecutionDetector
include ::Crystalball::MapGenerator::Helpers::PathFilter
attr_reader :exclude_prefixes
def initialize(*args, exclude_prefixes: [])
super(*args)
@exclude_prefixes = exclude_prefixes
end
# Detects files affected during example execution based on line coverage.
# Transforms absolute paths to relative.
# Exclude paths outside of repository and in excluded prefixes
#
# @param[Hash] hash of files affected before example execution
# @param[Hash] hash of files affected after example execution
# @return [Array<String>]
def detect(before, after)
file_names = after.keys
covered_files = file_names.reject { |file_name| same_coverage?(before, after, file_name) }
filter(covered_files)
end
private
def same_coverage?(before, after, file_name)
before[file_name] && before[file_name][:lines] == after[file_name][:lines]
end
def filter(paths)
super.reject do |file_name|
exclude_prefixes.any? { |prefix| file_name.start_with?(prefix) }
end
end
end
end
end
# frozen_string_literal: true
require 'coverage'
require 'crystalball/map_generator/coverage_strategy'
require_relative './coverage_lines_execution_detector'
module Tooling
module Crystalball
# Crystalball map generator strategy based on Crystalball::MapGenerator::CoverageStrategy,
# modified to use Coverage.start(lines: true)
# This maintains compatibility with SimpleCov on Ruby >= 2.5 with start arguments
# and SimpleCov.start uses Coverage.start(lines: true) by default
class CoverageLinesStrategy < ::Crystalball::MapGenerator::CoverageStrategy
def initialize(execution_detector = CoverageLinesExecutionDetector)
super(execution_detector)
end
def after_register
Coverage.start(lines: true)
end
end
end
end
# frozen_string_literal: true
require 'set'
require 'yaml'
module Tooling
class TestMapGenerator
def initialize
@mapping = Hash.new { |h, k| h[k] = Set.new }
end
def parse(yaml_files)
Array(yaml_files).each do |yaml_file|
data = File.read(yaml_file)
_metadata, example_groups = data.split("---\n").reject(&:empty?).map { |yml| YAML.safe_load(yml, [Symbol]) }
example_groups.each do |example_id, files|
files.each do |file|
spec_file = strip_example_uid(example_id)
@mapping[file] << spec_file
end
end
end
end
def mapping
@mapping.transform_values { |set| set.to_a }
end
private
def strip_example_uid(example_id)
example_id.gsub(/\[.+\]/, '')
end
end
end
# frozen_string_literal: true
module Tooling
class TestMapPacker
SEPARATOR = '/'.freeze
MARKER = 1
def pack(map)
map.transform_values(&method(:create_tree_from_tests))
end
def unpack(compact_map)
compact_map.transform_values(&method(:retrieve_tests_from_tree))
end
private
def create_tree_from_tests(tests)
tests.inject({}) do |tree, test|
segments = test.split(SEPARATOR)
branch = create_branch_from_segments(segments)
deep_merge(tree, branch)
end
end
def create_branch_from_segments(segments)
segments.reverse.inject(MARKER) { |node, parent| { parent => node } }
end
def deep_merge(hash, other)
hash.merge(other) do |_, this_val, other_val|
if this_val.is_a?(Hash) && other_val.is_a?(Hash)
deep_merge(this_val, other_val)
else
other_val
end
end
end
def retrieve_tests_from_tree(tree)
traverse(tree).inject([]) do |tests, test|
tests << test
end
end
def traverse(tree, segments = [], &block)
return to_enum(__method__, tree, segments) unless block_given?
if tree == MARKER
return yield segments.join(SEPARATOR)
end
tree.each do |key, value|
traverse(value, segments + [key], &block)
end
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment