Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
0993bbd2
Commit
0993bbd2
authored
Feb 10, 2021
by
GitLab Bot
Browse files
Options
Browse Files
Download
Plain Diff
Automatic merge of gitlab-org/gitlab master
parents
b8290149
bea8acc9
Changes
34
Hide whitespace changes
Inline
Side-by-side
Showing
34 changed files
with
510 additions
and
78 deletions
+510
-78
app/assets/javascripts/commit/pipelines/pipelines_table.vue
app/assets/javascripts/commit/pipelines/pipelines_table.vue
+2
-2
app/assets/javascripts/environments/components/environments_app.vue
.../javascripts/environments/components/environments_app.vue
+2
-2
app/assets/javascripts/environments/folder/environments_folder_view.vue
...ascripts/environments/folder/environments_folder_view.vue
+2
-2
app/assets/javascripts/environments/mixins/environments_pagination_api_mixin.js
.../environments/mixins/environments_pagination_api_mixin.js
+0
-1
app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
...scripts/pipelines/components/pipelines_list/pipelines.vue
+2
-2
app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
...ts/pipelines/components/test_reports/test_suite_table.vue
+5
-5
app/assets/javascripts/pipelines/mixins/pipelines_pagination_api_mixin.js
...cripts/pipelines/mixins/pipelines_pagination_api_mixin.js
+66
-0
app/assets/javascripts/pipelines/pipeline_details_bundle.js
app/assets/javascripts/pipelines/pipeline_details_bundle.js
+2
-1
app/assets/javascripts/pipelines/stores/test_reports/getters.js
...sets/javascripts/pipelines/stores/test_reports/getters.js
+8
-2
app/assets/javascripts/pipelines/stores/test_reports/state.js
...assets/javascripts/pipelines/stores/test_reports/state.js
+2
-1
app/assets/javascripts/pipelines/stores/test_reports/utils.js
...assets/javascripts/pipelines/stores/test_reports/utils.js
+9
-0
app/assets/javascripts/search_settings/components/search_settings.vue
...avascripts/search_settings/components/search_settings.vue
+33
-5
app/assets/javascripts/search_settings/mount.js
app/assets/javascripts/search_settings/mount.js
+4
-4
app/assets/javascripts/settings_panels.js
app/assets/javascripts/settings_panels.js
+21
-4
app/assets/stylesheets/framework/dropdowns.scss
app/assets/stylesheets/framework/dropdowns.scss
+1
-1
app/assets/stylesheets/startup/startup-dark.scss
app/assets/stylesheets/startup/startup-dark.scss
+1
-1
app/assets/stylesheets/startup/startup-general.scss
app/assets/stylesheets/startup/startup-general.scss
+1
-1
app/assets/stylesheets/startup/startup-signin.scss
app/assets/stylesheets/startup/startup-signin.scss
+1
-1
app/views/projects/pipelines/_with_tabs.html.haml
app/views/projects/pipelines/_with_tabs.html.haml
+2
-1
changelogs/unreleased/267344-test-report-file-name-link.yml
changelogs/unreleased/267344-test-report-file-name-link.yml
+5
-0
doc/operations/incident_management/integrations.md
doc/operations/incident_management/integrations.md
+2
-2
doc/user/admin_area/img/impersonate_user_button_v13_8.png
doc/user/admin_area/img/impersonate_user_button_v13_8.png
+0
-0
doc/user/admin_area/index.md
doc/user/admin_area/index.md
+13
-0
doc/user/project/releases/index.md
doc/user/project/releases/index.md
+10
-0
lib/gitlab/database/migrations/instrumentation.rb
lib/gitlab/database/migrations/instrumentation.rb
+47
-0
lib/tasks/gitlab/db.rake
lib/tasks/gitlab/db.rake
+32
-0
spec/frontend/pipelines/test_reports/stores/getters_spec.js
spec/frontend/pipelines/test_reports/stores/getters_spec.js
+8
-1
spec/frontend/pipelines/test_reports/stores/utils_spec.js
spec/frontend/pipelines/test_reports/stores/utils_spec.js
+15
-1
spec/frontend/pipelines/test_reports/test_suite_table_spec.js
.../frontend/pipelines/test_reports/test_suite_table_spec.js
+9
-1
spec/frontend/search_settings/components/search_settings_spec.js
...ontend/search_settings/components/search_settings_spec.js
+71
-29
spec/frontend/search_settings/mount_spec.js
spec/frontend/search_settings/mount_spec.js
+2
-3
spec/frontend/settings_panels_spec.js
spec/frontend/settings_panels_spec.js
+5
-5
spec/lib/gitlab/database/migrations/instrumentation_spec.rb
spec/lib/gitlab/database/migrations/instrumentation_spec.rb
+76
-0
spec/tasks/gitlab/db_rake_spec.rb
spec/tasks/gitlab/db_rake_spec.rb
+51
-0
No files found.
app/assets/javascripts/commit/pipelines/pipelines_table.vue
View file @
0993bbd2
...
...
@@ -6,7 +6,7 @@ import pipelinesMixin from '~/pipelines/mixins/pipelines';
import
eventHub
from
'
~/pipelines/event_hub
'
;
import
TablePagination
from
'
~/vue_shared/components/pagination/table_pagination.vue
'
;
import
{
getParameterByName
}
from
'
~/lib/utils/common_utils
'
;
import
CIPaginationMixin
from
'
~/vue_shared/mixins/ci
_pagination_api_mixin
'
;
import
PipelinesPaginationApiMixin
from
'
~/pipelines/mixins/pipelines
_pagination_api_mixin
'
;
export
default
{
components
:
{
...
...
@@ -16,7 +16,7 @@ export default {
GlModal
,
GlLink
,
},
mixins
:
[
pipelinesMixin
,
CIPagination
Mixin
],
mixins
:
[
pipelinesMixin
,
PipelinesPaginationApi
Mixin
],
props
:
{
endpoint
:
{
type
:
String
,
...
...
app/assets/javascripts/environments/components/environments_app.vue
View file @
0993bbd2
...
...
@@ -2,9 +2,9 @@
import
{
GlBadge
,
GlButton
,
GlModalDirective
,
GlTab
,
GlTabs
}
from
'
@gitlab/ui
'
;
import
{
deprecatedCreateFlash
as
Flash
}
from
'
~/flash
'
;
import
{
s__
}
from
'
~/locale
'
;
import
CIPaginationMixin
from
'
~/vue_shared/mixins/ci_pagination_api_mixin
'
;
import
eventHub
from
'
../event_hub
'
;
import
environmentsMixin
from
'
../mixins/environments_mixin
'
;
import
EnvironmentsPaginationApiMixin
from
'
../mixins/environments_pagination_api_mixin
'
;
import
emptyState
from
'
./empty_state.vue
'
;
import
EnableReviewAppModal
from
'
./enable_review_app_modal.vue
'
;
import
StopEnvironmentModal
from
'
./stop_environment_modal.vue
'
;
...
...
@@ -33,7 +33,7 @@ export default {
directives
:
{
'
gl-modal
'
:
GlModalDirective
,
},
mixins
:
[
CIPagination
Mixin
,
environmentsMixin
],
mixins
:
[
EnvironmentsPaginationApi
Mixin
,
environmentsMixin
],
props
:
{
endpoint
:
{
type
:
String
,
...
...
app/assets/javascripts/environments/folder/environments_folder_view.vue
View file @
0993bbd2
<
script
>
import
{
GlBadge
,
GlTab
,
GlTabs
}
from
'
@gitlab/ui
'
;
import
environmentsMixin
from
'
../mixins/environments_mixin
'
;
import
CIPaginationMixin
from
'
../../vue_shared/mixins/ci
_pagination_api_mixin
'
;
import
EnvironmentsPaginationApiMixin
from
'
../mixins/environments
_pagination_api_mixin
'
;
import
StopEnvironmentModal
from
'
../components/stop_environment_modal.vue
'
;
import
DeleteEnvironmentModal
from
'
../components/delete_environment_modal.vue
'
;
...
...
@@ -14,7 +14,7 @@ export default {
StopEnvironmentModal
,
},
mixins
:
[
environmentsMixin
,
CIPagination
Mixin
],
mixins
:
[
environmentsMixin
,
EnvironmentsPaginationApi
Mixin
],
props
:
{
endpoint
:
{
...
...
app/assets/javascripts/
vue_shared/mixins/ci
_pagination_api_mixin.js
→
app/assets/javascripts/
environments/mixins/environments
_pagination_api_mixin.js
View file @
0993bbd2
/**
* API callbacks for pagination and tabs
* shared between Pipelines and Environments table.
*
* Components need to have `scope`, `page` and `requestData`
*/
...
...
app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
View file @
0993bbd2
...
...
@@ -6,7 +6,7 @@ import { deprecatedCreateFlash as createFlash } from '~/flash';
import
TablePagination
from
'
~/vue_shared/components/pagination/table_pagination.vue
'
;
import
NavigationTabs
from
'
~/vue_shared/components/navigation_tabs.vue
'
;
import
{
getParameterByName
}
from
'
~/lib/utils/common_utils
'
;
import
CIPaginationMixin
from
'
~/vue_shared/mixins/ci
_pagination_api_mixin
'
;
import
PipelinesPaginationApiMixin
from
'
../../mixins/pipelines
_pagination_api_mixin
'
;
import
pipelinesMixin
from
'
../../mixins/pipelines
'
;
import
PipelinesService
from
'
../../services/pipelines_service
'
;
import
{
validateParams
}
from
'
../../utils
'
;
...
...
@@ -22,7 +22,7 @@ export default {
PipelinesFilteredSearch
,
GlIcon
,
},
mixins
:
[
pipelinesMixin
,
CIPagination
Mixin
],
mixins
:
[
pipelinesMixin
,
PipelinesPaginationApi
Mixin
],
props
:
{
store
:
{
type
:
Object
,
...
...
app/assets/javascripts/pipelines/components/test_reports/test_suite_table.vue
View file @
0993bbd2
...
...
@@ -5,6 +5,7 @@ import {
GlTooltipDirective
,
GlFriendlyWrap
,
GlIcon
,
GlLink
,
GlButton
,
GlPagination
,
}
from
'
@gitlab/ui
'
;
...
...
@@ -16,6 +17,7 @@ export default {
components
:
{
GlIcon
,
GlFriendlyWrap
,
GlLink
,
GlButton
,
GlPagination
,
TestCaseDetails
,
...
...
@@ -97,11 +99,9 @@ export default {
<div
class=
"table-section section-10 section-wrap"
>
<div
role=
"rowheader"
class=
"table-mobile-header"
>
{{
__
(
'
Filename
'
)
}}
</div>
<div
class=
"table-mobile-content gl-md-pr-2 gl-overflow-wrap-break"
>
<gl-friendly-wrap
v-if=
"testCase.file"
:symbols=
"$options.wrapSymbols"
:text=
"testCase.file"
/>
<gl-link
v-if=
"testCase.file"
:href=
"testCase.filePath"
target=
"_blank"
>
<gl-friendly-wrap
:symbols=
"$options.wrapSymbols"
:text=
"testCase.file"
/>
</gl-link>
<gl-button
v-if=
"testCase.file"
v-gl-tooltip
...
...
app/assets/javascripts/pipelines/mixins/pipelines_pagination_api_mixin.js
0 → 100644
View file @
0993bbd2
/**
* API callbacks for pagination and tabs
*
* Components need to have `scope`, `page` and `requestData`
*/
import
{
validateParams
}
from
'
~/pipelines/utils
'
;
import
{
historyPushState
,
buildUrlWithCurrentLocation
}
from
'
../../lib/utils/common_utils
'
;
export
default
{
methods
:
{
onChangeTab
(
scope
)
{
if
(
this
.
scope
===
scope
)
{
return
;
}
let
params
=
{
scope
,
page
:
'
1
'
,
};
params
=
this
.
onChangeWithFilter
(
params
);
this
.
updateContent
(
params
);
},
onChangePage
(
page
)
{
/* URLS parameters are strings, we need to parse to match types */
let
params
=
{
page
:
Number
(
page
).
toString
(),
};
if
(
this
.
scope
)
{
params
.
scope
=
this
.
scope
;
}
params
=
this
.
onChangeWithFilter
(
params
);
this
.
updateContent
(
params
);
},
onChangeWithFilter
(
params
)
{
return
{
...
params
,
...
validateParams
(
this
.
requestData
)
};
},
updateInternalState
(
parameters
)
{
// stop polling
this
.
poll
.
stop
();
const
queryString
=
Object
.
keys
(
parameters
)
.
map
((
parameter
)
=>
{
const
value
=
parameters
[
parameter
];
// update internal state for UI
this
[
parameter
]
=
value
;
return
`
${
parameter
}
=
${
encodeURIComponent
(
value
)}
`
;
})
.
join
(
'
&
'
);
// update polling parameters
this
.
requestData
=
parameters
;
historyPushState
(
buildUrlWithCurrentLocation
(
`?
${
queryString
}
`
));
this
.
isLoading
=
true
;
},
},
};
app/assets/javascripts/pipelines/pipeline_details_bundle.js
View file @
0993bbd2
...
...
@@ -58,8 +58,9 @@ const createLegacyPipelinesDetailApp = (mediator) => {
const
createTestDetails
=
()
=>
{
const
el
=
document
.
querySelector
(
SELECTORS
.
PIPELINE_TESTS
);
const
{
summaryEndpoint
,
suiteEndpoint
}
=
el
?.
dataset
||
{};
const
{
blobPath
,
summaryEndpoint
,
suiteEndpoint
}
=
el
?.
dataset
||
{};
const
testReportsStore
=
createTestReportsStore
({
blobPath
,
summaryEndpoint
,
suiteEndpoint
,
});
...
...
app/assets/javascripts/pipelines/stores/test_reports/getters.js
View file @
0993bbd2
import
{
addIconStatus
,
formattedTime
}
from
'
./utils
'
;
import
{
addIconStatus
,
format
FilePath
,
format
tedTime
}
from
'
./utils
'
;
export
const
getTestSuites
=
(
state
)
=>
{
const
{
test_suites
:
testSuites
=
[]
}
=
state
.
testReports
;
...
...
@@ -17,7 +17,13 @@ export const getSuiteTests = (state) => {
const
{
page
,
perPage
}
=
state
.
pageInfo
;
const
start
=
(
page
-
1
)
*
perPage
;
return
testCases
.
map
(
addIconStatus
).
slice
(
start
,
start
+
perPage
);
return
testCases
.
map
((
testCase
)
=>
({
...
testCase
,
filePath
:
testCase
.
file
?
`
${
state
.
blobPath
}
/
${
formatFilePath
(
testCase
.
file
)}
`
:
null
,
}))
.
map
(
addIconStatus
)
.
slice
(
start
,
start
+
perPage
);
};
export
const
getSuiteTestCount
=
(
state
)
=>
getSelectedSuite
(
state
)?.
test_cases
?.
length
||
0
;
app/assets/javascripts/pipelines/stores/test_reports/state.js
View file @
0993bbd2
export
default
({
summaryEndpoint
=
''
,
suiteEndpoint
=
''
})
=>
({
export
default
({
blobPath
=
''
,
summaryEndpoint
=
''
,
suiteEndpoint
=
''
})
=>
({
blobPath
,
summaryEndpoint
,
suiteEndpoint
,
testReports
:
{},
...
...
app/assets/javascripts/pipelines/stores/test_reports/utils.js
View file @
0993bbd2
import
{
__
,
sprintf
}
from
'
../../../locale
'
;
import
{
TestStatus
}
from
'
../../constants
'
;
/**
* Removes `./` from the beginning of a file path so it can be appended onto a blob path
* @param {String} file
* @returns {String} - formatted value
*/
export
function
formatFilePath
(
file
)
{
return
file
.
replace
(
/^
\.?\/
*/
,
''
);
}
export
function
iconForTestStatus
(
status
)
{
switch
(
status
)
{
case
TestStatus
.
SUCCESS
:
...
...
app/assets/javascripts/search_settings/components/search_settings.vue
View file @
0993bbd2
...
...
@@ -3,20 +3,37 @@ import { GlSearchBoxByType } from '@gitlab/ui';
import
{
uniq
}
from
'
lodash
'
;
import
{
EXCLUDED_NODES
,
HIDE_CLASS
,
HIGHLIGHT_CLASS
,
TYPING_DELAY
}
from
'
../constants
'
;
const
origExpansions
=
new
Map
();
const
findSettingsSection
=
(
sectionSelector
,
node
)
=>
{
return
node
.
parentElement
.
closest
(
sectionSelector
);
};
const
resetSections
=
({
sectionSelector
,
expandSection
,
collapseSection
})
=>
{
document
.
querySelectorAll
(
sectionSelector
).
forEach
((
section
,
index
)
=>
{
section
.
classList
.
remove
(
HIDE_CLASS
);
if
(
index
===
0
)
{
const
restoreExpansionState
=
({
expandSection
,
collapseSection
})
=>
{
origExpansions
.
forEach
((
isExpanded
,
section
)
=>
{
if
(
isExpanded
)
{
expandSection
(
section
);
}
else
{
collapseSection
(
section
);
}
});
origExpansions
.
clear
();
};
const
saveExpansionState
=
(
sections
,
{
isExpanded
})
=>
{
// If we've saved expansions before, don't override it.
if
(
origExpansions
.
size
>
0
)
{
return
;
}
sections
.
forEach
((
section
)
=>
origExpansions
.
set
(
section
,
isExpanded
(
section
)));
};
const
resetSections
=
({
sectionSelector
})
=>
{
document
.
querySelectorAll
(
sectionSelector
).
forEach
((
section
)
=>
{
section
.
classList
.
remove
(
HIDE_CLASS
);
});
};
const
clearHighlights
=
()
=>
{
...
...
@@ -85,6 +102,12 @@ export default {
type
:
String
,
required
:
true
,
},
isExpandedFn
:
{
type
:
Function
,
required
:
false
,
// default to a function that returns false
default
:
()
=>
()
=>
false
,
},
},
data
()
{
return
{
...
...
@@ -97,6 +120,7 @@ export default {
sectionSelector
:
this
.
sectionSelector
,
expandSection
:
this
.
expandSection
,
collapseSection
:
this
.
collapseSection
,
isExpanded
:
this
.
isExpandedFn
,
};
this
.
searchTerm
=
value
;
...
...
@@ -104,7 +128,11 @@ export default {
clearResults
(
displayOptions
);
if
(
value
.
length
)
{
saveExpansionState
(
document
.
querySelectorAll
(
this
.
sectionSelector
),
displayOptions
);
displayResults
(
displayOptions
,
search
(
this
.
searchRoot
,
value
));
}
else
{
restoreExpansionState
(
displayOptions
);
}
},
expandSection
(
section
)
{
...
...
app/assets/javascripts/search_settings/mount.js
View file @
0993bbd2
import
Vue
from
'
vue
'
;
import
$
from
'
jquery
'
;
import
{
expandSection
,
closeSection
}
from
'
~/settings_panels
'
;
import
{
expandSection
,
closeSection
,
isExpanded
}
from
'
~/settings_panels
'
;
import
SearchSettings
from
'
~/search_settings/components/search_settings.vue
'
;
const
mountSearch
=
({
el
})
=>
...
...
@@ -12,10 +11,11 @@ const mountSearch = ({ el }) =>
props
:
{
searchRoot
:
document
.
querySelector
(
'
#content-body
'
),
sectionSelector
:
'
.js-search-settings-section, section.settings
'
,
isExpandedFn
:
isExpanded
,
},
on
:
{
collapse
:
(
section
)
=>
closeSection
(
$
(
section
))
,
expand
:
(
section
)
=>
expandSection
(
$
(
section
))
,
collapse
:
closeSection
,
expand
:
expandSection
,
},
}),
});
...
...
app/assets/javascripts/settings_panels.js
View file @
0993bbd2
import
$
from
'
jquery
'
;
import
{
__
}
from
'
./locale
'
;
export
function
expandSection
(
$section
)
{
/**
* Returns true if the given section is expanded or not
*
* For legacy consistency, it supports both jQuery and DOM elements
*
* @param {jQuery | Element} section
*/
export
function
isExpanded
(
sectionArg
)
{
const
section
=
sectionArg
instanceof
$
?
sectionArg
[
0
]
:
sectionArg
;
return
section
.
classList
.
contains
(
'
expanded
'
);
}
export
function
expandSection
(
sectionArg
)
{
const
$section
=
$
(
sectionArg
);
$section
.
find
(
'
.js-settings-toggle:not(.js-settings-toggle-trigger-only)
'
).
text
(
__
(
'
Collapse
'
));
// eslint-disable-next-line @gitlab/no-global-event-off
$section
.
find
(
'
.settings-content
'
).
off
(
'
scroll.expandSection
'
).
scrollTop
(
0
);
...
...
@@ -13,7 +28,9 @@ export function expandSection($section) {
}
}
export
function
closeSection
(
$section
)
{
export
function
closeSection
(
sectionArg
)
{
const
$section
=
$
(
sectionArg
);
$section
.
find
(
'
.js-settings-toggle:not(.js-settings-toggle-trigger-only)
'
).
text
(
__
(
'
Expand
'
));
$section
.
find
(
'
.settings-content
'
).
on
(
'
scroll.expandSection
'
,
()
=>
expandSection
(
$section
));
$section
.
removeClass
(
'
expanded
'
);
...
...
@@ -26,7 +43,7 @@ export function closeSection($section) {
export
function
toggleSection
(
$section
)
{
$section
.
removeClass
(
'
no-animate
'
);
if
(
$section
.
hasClass
(
'
expanded
'
))
{
if
(
isExpanded
(
$section
))
{
closeSection
(
$section
);
}
else
{
expandSection
(
$section
);
...
...
@@ -38,7 +55,7 @@ export default function initSettingsPanels() {
const
$section
=
$
(
elm
);
$section
.
on
(
'
click.toggleSection
'
,
'
.js-settings-toggle
'
,
()
=>
toggleSection
(
$section
));
if
(
!
$section
.
hasClass
(
'
expanded
'
))
{
if
(
!
isExpanded
(
$section
))
{
$section
.
find
(
'
.settings-content
'
).
on
(
'
scroll.expandSection
'
,
()
=>
{
$section
.
removeClass
(
'
no-animate
'
);
expandSection
(
$section
);
...
...
app/assets/stylesheets/framework/dropdowns.scss
View file @
0993bbd2
...
...
@@ -326,7 +326,7 @@
color
:
$gl-text-color-secondary
;
}
.badge.badge-pill
+
span
:not
(
.badge.badge-pill
)
{
.badge.badge-pill
+
span
:not
(
.badge
)
:not
(
.badge-pill
)
{
// Expects up to 3 digits on the badge
margin-right
:
40px
;
}
...
...
app/assets/stylesheets/startup/startup-dark.scss
View file @
0993bbd2
...
...
@@ -903,7 +903,7 @@ table a code {
padding
:
0
;
background-color
:
#4f4f4f
;
}
.dropdown-menu
.badge.badge-pill
+
span
:not
(
.badge.badge-pill
)
{
.dropdown-menu
.badge.badge-pill
+
span
:not
(
.badge
)
:not
(
.badge-pill
)
{
margin-right
:
40px
;
}
.dropdown-select
{
...
...
app/assets/stylesheets/startup/startup-general.scss
View file @
0993bbd2
...
...
@@ -902,7 +902,7 @@ table a code {
padding
:
0
;
background-color
:
#dbdbdb
;
}
.dropdown-menu
.badge.badge-pill
+
span
:not
(
.badge.badge-pill
)
{
.dropdown-menu
.badge.badge-pill
+
span
:not
(
.badge
)
:not
(
.badge-pill
)
{
margin-right
:
40px
;
}
.dropdown-select
{
...
...
app/assets/stylesheets/startup/startup-signin.scss
View file @
0993bbd2
...
...
@@ -1174,7 +1174,7 @@ table a code {
padding
:
0
;
background-color
:
#dbdbdb
;
}
.dropdown-menu
.badge.badge-pill
+
span
:not
(
.badge.badge-pill
)
{
.dropdown-menu
.badge.badge-pill
+
span
:not
(
.badge
)
:not
(
.badge-pill
)
{
margin-right
:
40px
;
}
.dropdown-select
{
...
...
app/views/projects/pipelines/_with_tabs.html.haml
View file @
0993bbd2
...
...
@@ -82,5 +82,6 @@
#js-tab-tests
.tab-pane
#js-pipeline-tests-detail
{
data:
{
summary_endpoint:
summary_project_pipeline_tests_path
(
@project
,
@pipeline
,
format: :json
),
suite_endpoint:
project_pipeline_test_path
(
@project
,
@pipeline
,
suite_name:
'suite'
,
format: :json
)
}
}
suite_endpoint:
project_pipeline_test_path
(
@project
,
@pipeline
,
suite_name:
'suite'
,
format: :json
),
blob_path:
project_blob_path
(
@project
,
@pipeline
.
sha
)
}
}
=
render_if_exists
"projects/pipelines/tabs_content"
,
pipeline:
@pipeline
,
project:
@project
changelogs/unreleased/267344-test-report-file-name-link.yml
0 → 100644
View file @
0993bbd2
---
title
:
Add link to test case file in pipeline test report
merge_request
:
53650
author
:
type
:
added
doc/operations/incident_management/integrations.md
View file @
0993bbd2
...
...
@@ -43,7 +43,7 @@ receive alert payloads in JSON format. You can always
1.
Toggle the
**Active**
alert setting to display the
**URL**
and
**Authorization Key**
for the webhook configuration.
### HTTP Endpoints **
PREMIUM
**
### HTTP Endpoints **
(PREMIUM)
**
> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4442) in GitLab Premium 13.6.
...
...
@@ -72,7 +72,7 @@ side of the integrations list.
### External Prometheus integration
For GitLab versions 13.1 and greater,
please
read
For GitLab versions 13.1 and greater, read
[
External Prometheus Instances
](
../metrics/alerts.md#external-prometheus-instances
)
to configure alerts for this integration.
...
...
doc/user/admin_area/img/impersonate_user_button_v13_8.png
0 → 100644
View file @
0993bbd2
44.3 KB
doc/user/admin_area/index.md
View file @
0993bbd2
...
...
@@ -144,6 +144,19 @@ To search for users, enter your criteria in the search field. The user search is
insensitive, and applies partial matching to name and username. To search for an email address,
you must provide the complete email address.
#### User impersonation
An administrator can "impersonate" any other user, including other administrator users.
This allows the administrator to "see what the user sees," and take actions on behalf of the user.
You can impersonate a user in the following ways:
-
Through the UI, by selecting
**Admin Area > Overview > Users > [Select a user] > Impersonate**
.
-
With the API, using
[
impersonation tokens
](
../../api/README.md#impersonation-tokens
)
.
All impersonation activities are
[
captured with audit events
](
../../administration/audit_events.md#impersonation-data
)
.
![
user impersonation button
](
img/impersonate_user_button_v13_8.png
)
#### Users statistics
The
**Users statistics**
page provides an overview of user accounts by role. These statistics are
...
...
doc/user/project/releases/index.md
View file @
0993bbd2
...
...
@@ -475,6 +475,16 @@ terminal.
Read the
[
Release CLI documentation
](
https://gitlab.com/gitlab-org/release-cli/-/blob/master/docs/index.md
)
for details.
## Release Metrics **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/259703) in GitLab Premium 13.9.
Group-level release metrics are available by navigating to
**Group > Analytics > CI/CD**
.
These metrics include:
-
Total number of releases in the group
-
Percentage of projects in the group that have at least one release
<!-- ## Troubleshooting
Include any troubleshooting steps that you can foresee. If you know beforehand what issues
...
...
lib/gitlab/database/migrations/instrumentation.rb
0 → 100644
View file @
0993bbd2
# frozen_string_literal: true
module
Gitlab
module
Database
module
Migrations
Observation
=
Struct
.
new
(
:migration
,
:walltime
,
:success
)
class
Instrumentation
attr_reader
:observations
def
initialize
@observations
=
[]
end
def
observe
(
migration
,
&
block
)
observation
=
Observation
.
new
(
migration
)
observation
.
success
=
true
exception
=
nil
observation
.
walltime
=
Benchmark
.
realtime
do
yield
rescue
=>
e
exception
=
e
observation
.
success
=
false
end
record_observation
(
observation
)
raise
exception
if
exception
observation
end
private
def
record_observation
(
observation
)
@observations
<<
observation
end
end
end
end
end
lib/tasks/gitlab/db.rake
View file @
0993bbd2
...
...
@@ -231,5 +231,37 @@ namespace :gitlab do
puts
"Found user created projects. Database active"
exit
0
end
desc
'Run migrations with instrumentation'
task
:migration_testing
,
[
:result_file
]
=>
:environment
do
|
_
,
args
|
result_file
=
args
[
:result_file
]
||
raise
(
"Please specify result_file argument"
)
raise
"File exists already, won't overwrite:
#{
result_file
}
"
if
File
.
exist?
(
result_file
)
verbose_was
,
ActiveRecord
::
Migration
.
verbose
=
ActiveRecord
::
Migration
.
verbose
,
true
ctx
=
ActiveRecord
::
Base
.
connection
.
migration_context
existing_versions
=
ctx
.
get_all_versions
.
to_set
pending_migrations
=
ctx
.
migrations
.
reject
do
|
migration
|
existing_versions
.
include?
(
migration
.
version
)
end
instrumentation
=
Gitlab
::
Database
::
Migrations
::
Instrumentation
.
new
pending_migrations
.
each
do
|
migration
|
instrumentation
.
observe
(
migration
.
version
)
do
ActiveRecord
::
Migrator
.
new
(
:up
,
ctx
.
migrations
,
ctx
.
schema_migration
,
migration
.
version
).
run
end
end
ensure
if
instrumentation
File
.
open
(
result_file
,
'wb+'
)
do
|
io
|
io
<<
instrumentation
.
observations
.
to_json
end
end
ActiveRecord
::
Base
.
clear_cache!
ActiveRecord
::
Migration
.
verbose
=
verbose_was
end
end
end
spec/frontend/pipelines/test_reports/stores/getters_spec.js
View file @
0993bbd2
import
{
getJSONFixture
}
from
'
helpers/fixtures
'
;
import
*
as
getters
from
'
~/pipelines/stores/test_reports/getters
'
;
import
{
iconForTestStatus
,
formattedTime
}
from
'
~/pipelines/stores/test_reports/utils
'
;
import
{
iconForTestStatus
,
formatFilePath
,
formattedTime
,
}
from
'
~/pipelines/stores/test_reports/utils
'
;
describe
(
'
Getters TestReports Store
'
,
()
=>
{
let
state
;
...
...
@@ -8,6 +12,7 @@ describe('Getters TestReports Store', () => {
const
testReports
=
getJSONFixture
(
'
pipelines/test_report.json
'
);
const
defaultState
=
{
blobPath
:
'
/test/blob/path
'
,
testReports
,
selectedSuiteIndex
:
0
,
pageInfo
:
{
...
...
@@ -17,6 +22,7 @@ describe('Getters TestReports Store', () => {
};
const
emptyState
=
{
blobPath
:
''
,
testReports
:
{},
selectedSuite
:
null
,
pageInfo
:
{
...
...
@@ -74,6 +80,7 @@ describe('Getters TestReports Store', () => {
const
expected
=
testReports
.
test_suites
[
0
].
test_cases
.
map
((
x
)
=>
({
...
x
,
filePath
:
`
${
state
.
blobPath
}
/
${
formatFilePath
(
x
.
file
)}
`
,
formattedTime
:
formattedTime
(
x
.
execution_time
),
icon
:
iconForTestStatus
(
x
.
status
),
}))
...
...
spec/frontend/pipelines/test_reports/stores/utils_spec.js
View file @
0993bbd2
import
{
formattedTime
}
from
'
~/pipelines/stores/test_reports/utils
'
;
import
{
format
FilePath
,
format
tedTime
}
from
'
~/pipelines/stores/test_reports/utils
'
;
describe
(
'
Test reports utils
'
,
()
=>
{
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
(
formatFilePath
(
file
)).
toBe
(
expected
);
});
});
describe
(
'
formattedTime
'
,
()
=>
{
describe
(
'
when time is smaller than a second
'
,
()
=>
{
it
(
'
should return time in milliseconds fixed to 2 decimals
'
,
()
=>
{
...
...
spec/frontend/pipelines/test_reports/test_suite_table_spec.js
View file @
0993bbd2
import
Vuex
from
'
vuex
'
;
import
{
shallowMount
,
createLocalVue
}
from
'
@vue/test-utils
'
;
import
{
GlButton
,
GlFriendlyWrap
,
GlPagination
}
from
'
@gitlab/ui
'
;
import
{
GlButton
,
GlFriendlyWrap
,
Gl
Link
,
Gl
Pagination
}
from
'
@gitlab/ui
'
;
import
{
getJSONFixture
}
from
'
helpers/fixtures
'
;
import
SuiteTable
from
'
~/pipelines/components/test_reports/test_suite_table.vue
'
;
import
*
as
getters
from
'
~/pipelines/stores/test_reports/getters
'
;
import
{
formatFilePath
}
from
'
~/pipelines/stores/test_reports/utils
'
;
import
{
TestStatus
}
from
'
~/pipelines/constants
'
;
import
skippedTestCases
from
'
./mock_data
'
;
...
...
@@ -20,15 +21,18 @@ describe('Test reports suite table', () => {
testSuite
.
test_cases
=
[...
testSuite
.
test_cases
,
...
skippedTestCases
];
const
testCases
=
testSuite
.
test_cases
;
const
blobPath
=
'
/test/blob/path
'
;
const
noCasesMessage
=
()
=>
wrapper
.
find
(
'
.js-no-test-cases
'
);
const
allCaseRows
=
()
=>
wrapper
.
findAll
(
'
.js-case-row
'
);
const
findCaseRowAtIndex
=
(
index
)
=>
wrapper
.
findAll
(
'
.js-case-row
'
).
at
(
index
);
const
findLinkForRow
=
(
row
)
=>
row
.
find
(
GlLink
);
const
findIconForRow
=
(
row
,
status
)
=>
row
.
find
(
`.ci-status-icon-
${
status
}
`
);
const
createComponent
=
(
suite
=
testSuite
,
perPage
=
20
)
=>
{
store
=
new
Vuex
.
Store
({
state
:
{
blobPath
,
testReports
:
{
test_suites
:
[
suite
],
},
...
...
@@ -82,9 +86,13 @@ describe('Test reports suite table', () => {
it
(
'
renders the file name for the test with a copy button
'
,
()
=>
{
const
{
file
}
=
testCases
[
0
];
const
relativeFile
=
formatFilePath
(
file
);
const
filePath
=
`
${
blobPath
}
/
${
relativeFile
}
`
;
const
row
=
findCaseRowAtIndex
(
0
);
const
fileLink
=
findLinkForRow
(
row
);
const
button
=
row
.
find
(
GlButton
);
expect
(
fileLink
.
attributes
(
'
href
'
)).
toBe
(
filePath
);
expect
(
row
.
text
()).
toContain
(
file
);
expect
(
button
.
exists
()).
toBe
(
true
);
expect
(
button
.
attributes
(
'
data-clipboard-text
'
)).
toBe
(
file
);
...
...
spec/frontend/search_settings/components/search_settings_spec.js
View file @
0993bbd2
...
...
@@ -2,6 +2,7 @@ import { GlSearchBoxByType } from '@gitlab/ui';
import
{
shallowMount
}
from
'
@vue/test-utils
'
;
import
SearchSettings
from
'
~/search_settings/components/search_settings.vue
'
;
import
{
HIGHLIGHT_CLASS
,
HIDE_CLASS
}
from
'
~/search_settings/constants
'
;
import
{
isExpanded
,
expandSection
,
closeSection
}
from
'
~/settings_panels
'
;
describe
(
'
search_settings/components/search_settings.vue
'
,
()
=>
{
const
ROOT_ID
=
'
content-body
'
;
...
...
@@ -9,6 +10,8 @@ describe('search_settings/components/search_settings.vue', () => {
const
SEARCH_TERM
=
'
Delete project
'
;
const
GENERAL_SETTINGS_ID
=
'
js-general-settings
'
;
const
ADVANCED_SETTINGS_ID
=
'
js-advanced-settings
'
;
const
EXTRA_SETTINGS_ID
=
'
js-extra-settings
'
;
let
wrapper
;
const
buildWrapper
=
()
=>
{
...
...
@@ -16,10 +19,15 @@ describe('search_settings/components/search_settings.vue', () => {
propsData
:
{
searchRoot
:
document
.
querySelector
(
`#
${
ROOT_ID
}
`
),
sectionSelector
:
SECTION_SELECTOR
,
isExpandedFn
:
isExpanded
,
},
// Add real listeners so we can simplify and strengthen some tests.
listeners
:
{
expand
:
expandSection
,
collapse
:
closeSection
,
},
});
};
const
sections
=
()
=>
Array
.
from
(
document
.
querySelectorAll
(
SECTION_SELECTOR
));
const
sectionsCount
=
()
=>
sections
().
length
;
const
visibleSectionsCount
=
()
=>
...
...
@@ -39,7 +47,10 @@ describe('search_settings/components/search_settings.vue', () => {
<section id="
${
GENERAL_SETTINGS_ID
}
" class="settings">
<span>General</span>
</section>
<section id="
${
ADVANCED_SETTINGS_ID
}
" class="settings">
<section id="
${
ADVANCED_SETTINGS_ID
}
" class="settings expanded">
<span>Advanced</span>
</section>
<section id="
${
EXTRA_SETTINGS_ID
}
" class="settings">
<span>
${
SEARCH_TERM
}
</span>
</section>
</div>
...
...
@@ -52,17 +63,6 @@ describe('search_settings/components/search_settings.vue', () => {
wrapper
.
destroy
();
});
it
(
'
expands first section and collapses the rest
'
,
()
=>
{
clearSearch
();
const
[
firstSection
,
...
otherSections
]
=
sections
();
expect
(
wrapper
.
emitted
()).
toEqual
({
expand
:
[[
firstSection
]],
collapse
:
otherSections
.
map
((
x
)
=>
[
x
]),
});
});
it
(
'
hides sections that do not match the search term
'
,
()
=>
{
const
hiddenSection
=
document
.
querySelector
(
`#
${
GENERAL_SETTINGS_ID
}
`
);
search
(
SEARCH_TERM
);
...
...
@@ -72,12 +72,11 @@ describe('search_settings/components/search_settings.vue', () => {
});
it
(
'
expands section that matches the search term
'
,
()
=>
{
const
section
=
document
.
querySelector
(
`#
${
ADVANCED
_SETTINGS_ID
}
`
);
const
section
=
document
.
querySelector
(
`#
${
EXTRA
_SETTINGS_ID
}
`
);
search
(
SEARCH_TERM
);
// Last called because expand is always called once to reset the page state
expect
(
wrapper
.
emitted
().
expand
[
1
][
0
]).
toBe
(
section
);
expect
(
wrapper
.
emitted
(
'
expand
'
)).
toEqual
([[
section
]]);
});
it
(
'
highlight elements that match the search term
'
,
()
=>
{
...
...
@@ -86,21 +85,64 @@ describe('search_settings/components/search_settings.vue', () => {
expect
(
highlightedElementsCount
()).
toBe
(
1
);
});
describe
(
'
when search term is cleared
'
,
()
=>
{
beforeEach
(()
=>
{
search
(
SEARCH_TERM
);
});
it
(
'
displays all sections
'
,
()
=>
{
expect
(
visibleSectionsCount
()).
toBe
(
1
);
clearSearch
();
expect
(
visibleSectionsCount
()).
toBe
(
sectionsCount
());
describe
(
'
default
'
,
()
=>
{
it
(
'
test setup starts with expansion state
'
,
()
=>
{
expect
(
sections
().
map
(
isExpanded
)).
toEqual
([
false
,
true
,
false
]);
});
it
(
'
removes the highlight from all elements
'
,
()
=>
{
expect
(
highlightedElementsCount
()).
toBe
(
1
);
clearSearch
();
expect
(
highlightedElementsCount
()).
toBe
(
0
);
describe
(
'
when searched and cleared
'
,
()
=>
{
beforeEach
(()
=>
{
search
(
'
Test
'
);
clearSearch
();
});
it
(
'
displays all sections
'
,
()
=>
{
expect
(
visibleSectionsCount
()).
toBe
(
sectionsCount
());
});
it
(
'
removes the highlight from all elements
'
,
()
=>
{
expect
(
highlightedElementsCount
()).
toBe
(
0
);
});
it
(
'
should preserve original expansion state
'
,
()
=>
{
expect
(
sections
().
map
(
isExpanded
)).
toEqual
([
false
,
true
,
false
]);
});
it
(
'
should preserve state by emitting events
'
,
()
=>
{
const
[
first
,
mid
,
last
]
=
sections
();
expect
(
wrapper
.
emitted
()).
toEqual
({
expand
:
[[
mid
]],
collapse
:
[[
first
],
[
last
]],
});
});
describe
(
'
after multiple searches and clear
'
,
()
=>
{
beforeEach
(()
=>
{
search
(
'
Test
'
);
search
(
SEARCH_TERM
);
clearSearch
();
});
it
(
'
should preserve last expansion state
'
,
()
=>
{
expect
(
sections
().
map
(
isExpanded
)).
toEqual
([
false
,
true
,
false
]);
});
});
describe
(
'
after user expands and collapses, search, and clear
'
,
()
=>
{
beforeEach
(()
=>
{
const
[
first
,
mid
]
=
sections
();
closeSection
(
mid
);
expandSection
(
first
);
search
(
SEARCH_TERM
);
clearSearch
();
});
it
(
'
should preserve last expansion state
'
,
()
=>
{
expect
(
sections
().
map
(
isExpanded
)).
toEqual
([
true
,
false
,
false
]);
});
});
});
});
});
spec/frontend/search_settings/mount_spec.js
View file @
0993bbd2
import
$
from
'
jquery
'
;
import
{
setHTMLFixture
}
from
'
helpers/fixtures
'
;
import
mount
from
'
~/search_settings/mount
'
;
import
{
expandSection
,
closeSection
}
from
'
~/settings_panels
'
;
...
...
@@ -24,13 +23,13 @@ describe('search_settings/mount', () => {
const
section
=
{
name
:
'
section
'
};
app
.
$refs
.
searchSettings
.
$emit
(
'
expand
'
,
section
);
expect
(
expandSection
).
toHaveBeenCalledWith
(
$
(
section
)
);
expect
(
expandSection
).
toHaveBeenCalledWith
(
section
);
});
it
(
'
calls settings_panel.closeSection when collapse event is emitted
'
,
()
=>
{
const
section
=
{
name
:
'
section
'
};
app
.
$refs
.
searchSettings
.
$emit
(
'
collapse
'
,
section
);
expect
(
closeSection
).
toHaveBeenCalledWith
(
$
(
section
)
);
expect
(
closeSection
).
toHaveBeenCalledWith
(
section
);
});
});
spec/frontend/settings_panels_spec.js
View file @
0993bbd2
import
$
from
'
jquery
'
;
import
initSettingsPanels
from
'
~/settings_panels
'
;
import
initSettingsPanels
,
{
isExpanded
}
from
'
~/settings_panels
'
;
describe
(
'
Settings Panels
'
,
()
=>
{
preloadFixtures
(
'
groups/edit.html
'
);
...
...
@@ -20,11 +20,11 @@ describe('Settings Panels', () => {
// Our test environment automatically expands everything so we need to clear that out first
panel
.
classList
.
remove
(
'
expanded
'
);
expect
(
panel
.
classList
.
contains
(
'
expanded
'
)).
toBe
(
false
);
expect
(
isExpanded
(
panel
)).
toBe
(
false
);
initSettingsPanels
();
expect
(
panel
.
classList
.
contains
(
'
expanded
'
)).
toBe
(
true
);
expect
(
isExpanded
(
panel
)).
toBe
(
true
);
});
});
...
...
@@ -35,11 +35,11 @@ describe('Settings Panels', () => {
initSettingsPanels
();
expect
(
panel
.
classList
.
contains
(
'
expanded
'
)).
toBe
(
true
);
expect
(
isExpanded
(
panel
)).
toBe
(
true
);
$
(
trigger
).
click
();
expect
(
panel
.
classList
.
contains
(
'
expanded
'
)).
toBe
(
false
);
expect
(
isExpanded
(
panel
)).
toBe
(
false
);
expect
(
trigger
.
textContent
).
toEqual
(
originalText
);
});
});
spec/lib/gitlab/database/migrations/instrumentation_spec.rb
0 → 100644
View file @
0993bbd2
# frozen_string_literal: true
require
'spec_helper'
RSpec
.
describe
Gitlab
::
Database
::
Migrations
::
Instrumentation
do
describe
'#observe'
do
subject
{
described_class
.
new
}
let
(
:migration
)
{
1234
}
it
'executes the given block'
do
expect
{
|
b
|
subject
.
observe
(
migration
,
&
b
)
}.
to
yield_control
end
context
'on successful execution'
do
subject
{
described_class
.
new
.
observe
(
migration
)
{}
}
it
'records walltime'
do
expect
(
subject
.
walltime
).
not_to
be_nil
end
it
'records success'
do
expect
(
subject
.
success
).
to
be_truthy
end
it
'records the migration version'
do
expect
(
subject
.
migration
).
to
eq
(
migration
)
end
end
context
'upon failure'
do
subject
{
described_class
.
new
.
observe
(
migration
)
{
raise
'something went wrong'
}
}
it
'raises the exception'
do
expect
{
subject
}.
to
raise_error
(
/something went wrong/
)
end
context
'retrieving observations'
do
subject
{
instance
.
observations
.
first
}
before
do
instance
.
observe
(
migration
)
{
raise
'something went wrong'
}
rescue
# ignore
end
let
(
:instance
)
{
described_class
.
new
}
it
'records walltime'
do
expect
(
subject
.
walltime
).
not_to
be_nil
end
it
'records failure'
do
expect
(
subject
.
success
).
to
be_falsey
end
it
'records the migration version'
do
expect
(
subject
.
migration
).
to
eq
(
migration
)
end
end
end
context
'sequence of migrations with failures'
do
subject
{
described_class
.
new
}
let
(
:migration1
)
{
double
(
'migration1'
,
call:
nil
)
}
let
(
:migration2
)
{
double
(
'migration2'
,
call:
nil
)
}
it
'records observations for all migrations'
do
subject
.
observe
(
'migration1'
)
{}
subject
.
observe
(
'migration2'
)
{
raise
'something went wrong'
}
rescue
nil
expect
(
subject
.
observations
.
size
).
to
eq
(
2
)
end
end
end
end
spec/tasks/gitlab/db_rake_spec.rb
View file @
0993bbd2
...
...
@@ -297,6 +297,57 @@ RSpec.describe 'gitlab:db namespace rake task' do
end
end
describe
'#migrate_with_instrumentation'
do
subject
{
run_rake_task
(
'gitlab:db:migration_testing'
,
"[
#{
filename
}
]"
)
}
let
(
:ctx
)
{
double
(
'ctx'
,
migrations:
all_migrations
,
schema_migration:
double
,
get_all_versions:
existing_versions
)
}
let
(
:instrumentation
)
{
instance_double
(
Gitlab
::
Database
::
Migrations
::
Instrumentation
,
observations:
observations
)
}
let
(
:existing_versions
)
{
[
1
]
}
let
(
:all_migrations
)
{
[
double
(
'migration1'
,
version:
1
),
pending_migration
]
}
let
(
:pending_migration
)
{
double
(
'migration2'
,
version:
2
)
}
let
(
:filename
)
{
'results-file.json'
}
let
(
:buffer
)
{
StringIO
.
new
}
let
(
:observations
)
{
%w[some data]
}
before
do
allow
(
ActiveRecord
::
Base
.
connection
).
to
receive
(
:migration_context
).
and_return
(
ctx
)
allow
(
Gitlab
::
Database
::
Migrations
::
Instrumentation
).
to
receive
(
:new
).
and_return
(
instrumentation
)
allow
(
ActiveRecord
::
Migrator
).
to
receive_message_chain
(
'new.run'
).
with
(
any_args
).
with
(
no_args
)
allow
(
instrumentation
).
to
receive
(
:observe
).
and_yield
allow
(
File
).
to
receive
(
:open
).
with
(
filename
,
'wb+'
).
and_yield
(
buffer
)
end
it
'fails when given no filename argument'
do
expect
{
run_rake_task
(
'gitlab:db:migration_testing'
)
}.
to
raise_error
(
/specify result_file/
)
end
it
'fails when the given file already exists'
do
expect
(
File
).
to
receive
(
:exist?
).
with
(
filename
).
and_return
(
true
)
expect
{
subject
}.
to
raise_error
(
/File exists/
)
end
it
'instruments the pending migration'
do
expect
(
instrumentation
).
to
receive
(
:observe
).
with
(
2
).
and_yield
subject
end
it
'executes the pending migration'
do
expect
(
ActiveRecord
::
Migrator
).
to
receive_message_chain
(
'new.run'
).
with
(
:up
,
ctx
.
migrations
,
ctx
.
schema_migration
,
pending_migration
.
version
).
with
(
no_args
)
subject
end
it
'writes observations out to JSON file'
do
subject
expect
(
buffer
.
string
).
to
eq
(
observations
.
to_json
)
end
end
def
run_rake_task
(
task_name
,
arguments
=
''
)
Rake
::
Task
[
task_name
].
reenable
Rake
.
application
.
invoke_task
(
"
#{
task_name
}#{
arguments
}
"
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment