summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorChris Baumbauer <cab@cabnetworks.net>2019-04-06 02:02:39 +0000
committerMike Greiling <mike@pixelcog.com>2019-04-06 02:02:39 +0000
commitb77fe7db3e885edca14c862f362e2bbd43f0e498 (patch)
treecd984b8bb900a6b3e37c8f6106101ba8617bf524 /spec
parent8e33e7cf474b61bbc684d993d86cb5aa775a01d0 (diff)
downloadgitlab-ce-b77fe7db3e885edca14c862f362e2bbd43f0e498.tar.gz
Add Knative metrics to Prometheus
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/projects/serverless/functions_controller_spec.rb9
-rw-r--r--spec/features/projects/serverless/functions_spec.rb2
-rw-r--r--spec/finders/projects/serverless/functions_finder_spec.rb32
-rw-r--r--spec/javascripts/serverless/components/area_spec.js121
-rw-r--r--spec/javascripts/serverless/components/environment_row_spec.js41
-rw-r--r--spec/javascripts/serverless/components/function_details_spec.js113
-rw-r--r--spec/javascripts/serverless/components/function_row_spec.js13
-rw-r--r--spec/javascripts/serverless/components/functions_spec.js113
-rw-r--r--spec/javascripts/serverless/components/missing_prometheus_spec.js37
-rw-r--r--spec/javascripts/serverless/components/pod_box_spec.js22
-rw-r--r--spec/javascripts/serverless/components/url_spec.js20
-rw-r--r--spec/javascripts/serverless/mock_data.js57
-rw-r--r--spec/javascripts/serverless/store/actions_spec.js88
-rw-r--r--spec/javascripts/serverless/store/getters_spec.js43
-rw-r--r--spec/javascripts/serverless/store/mutations_spec.js86
-rw-r--r--spec/javascripts/serverless/stores/serverless_store_spec.js36
-rw-r--r--spec/javascripts/serverless/utils.js20
-rw-r--r--spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb26
-rw-r--r--spec/models/serverless/function_spec.rb21
-rw-r--r--spec/support/helpers/prometheus_helpers.rb4
20 files changed, 775 insertions, 129 deletions
diff --git a/spec/controllers/projects/serverless/functions_controller_spec.rb b/spec/controllers/projects/serverless/functions_controller_spec.rb
index 276cf340962..782f5f272d9 100644
--- a/spec/controllers/projects/serverless/functions_controller_spec.rb
+++ b/spec/controllers/projects/serverless/functions_controller_spec.rb
@@ -76,6 +76,15 @@ describe Projects::Serverless::FunctionsController do
end
end
+ describe 'GET #metrics' do
+ context 'invalid data' do
+ it 'has a bad function name' do
+ get :metrics, params: params({ format: :json, environment_id: "*", id: "foo" })
+ expect(response).to have_gitlab_http_status(204)
+ end
+ end
+ end
+
describe 'GET #index with data', :use_clean_rails_memory_store_caching do
before do
stub_kubeclient_service_pods
diff --git a/spec/features/projects/serverless/functions_spec.rb b/spec/features/projects/serverless/functions_spec.rb
index aa71669de98..e14934b1672 100644
--- a/spec/features/projects/serverless/functions_spec.rb
+++ b/spec/features/projects/serverless/functions_spec.rb
@@ -50,7 +50,7 @@ describe 'Functions', :js do
end
it 'sees an empty listing of serverless functions' do
- expect(page).to have_selector('.gl-responsive-table-row')
+ expect(page).to have_selector('.empty-state')
end
end
end
diff --git a/spec/finders/projects/serverless/functions_finder_spec.rb b/spec/finders/projects/serverless/functions_finder_spec.rb
index 35279906854..3ad38207da4 100644
--- a/spec/finders/projects/serverless/functions_finder_spec.rb
+++ b/spec/finders/projects/serverless/functions_finder_spec.rb
@@ -4,6 +4,7 @@ require 'spec_helper'
describe Projects::Serverless::FunctionsFinder do
include KubernetesHelpers
+ include PrometheusHelpers
include ReactiveCachingHelpers
let(:user) { create(:user) }
@@ -24,12 +25,12 @@ describe Projects::Serverless::FunctionsFinder do
describe 'retrieve data from knative' do
it 'does not have knative installed' do
- expect(described_class.new(project.clusters).execute).to be_empty
+ expect(described_class.new(project).execute).to be_empty
end
context 'has knative installed' do
let!(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
- let(:finder) { described_class.new(project.clusters) }
+ let(:finder) { described_class.new(project) }
it 'there are no functions' do
expect(finder.execute).to be_empty
@@ -58,13 +59,36 @@ describe Projects::Serverless::FunctionsFinder do
expect(result).not_to be_empty
expect(result["metadata"]["name"]).to be_eql(cluster.project.name)
end
+
+ it 'has metrics', :use_clean_rails_memory_store_caching do
+ end
+ end
+
+ context 'has prometheus' do
+ let(:prometheus_adapter) { double('prometheus_adapter', can_query?: true) }
+ let!(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
+ let!(:prometheus) { create(:clusters_applications_prometheus, :installed, cluster: cluster) }
+ let(:finder) { described_class.new(project) }
+
+ before do
+ allow(finder).to receive(:prometheus_adapter).and_return(prometheus_adapter)
+ allow(prometheus_adapter).to receive(:query).and_return(prometheus_empty_body('matrix'))
+ end
+
+ it 'is available' do
+ expect(finder.has_prometheus?("*")).to be true
+ end
+
+ it 'has query data' do
+ expect(finder.invocation_metrics("*", cluster.project.name)).not_to be_nil
+ end
end
end
describe 'verify if knative is installed' do
context 'knative is not installed' do
it 'does not have knative installed' do
- expect(described_class.new(project.clusters).installed?).to be false
+ expect(described_class.new(project).installed?).to be false
end
end
@@ -72,7 +96,7 @@ describe Projects::Serverless::FunctionsFinder do
let!(:knative) { create(:clusters_applications_knative, :installed, cluster: cluster) }
it 'does have knative installed' do
- expect(described_class.new(project.clusters).installed?).to be true
+ expect(described_class.new(project).installed?).to be true
end
end
end
diff --git a/spec/javascripts/serverless/components/area_spec.js b/spec/javascripts/serverless/components/area_spec.js
new file mode 100644
index 00000000000..2be6ac3d268
--- /dev/null
+++ b/spec/javascripts/serverless/components/area_spec.js
@@ -0,0 +1,121 @@
+import { shallowMount } from '@vue/test-utils';
+import Area from '~/serverless/components/area.vue';
+import { mockNormalizedMetrics } from '../mock_data';
+
+describe('Area component', () => {
+ const mockWidgets = 'mockWidgets';
+ const mockGraphData = mockNormalizedMetrics;
+ let areaChart;
+
+ beforeEach(() => {
+ areaChart = shallowMount(Area, {
+ propsData: {
+ graphData: mockGraphData,
+ containerWidth: 0,
+ },
+ slots: {
+ default: mockWidgets,
+ },
+ });
+ });
+
+ afterEach(() => {
+ areaChart.destroy();
+ });
+
+ it('renders chart title', () => {
+ expect(areaChart.find({ ref: 'graphTitle' }).text()).toBe(mockGraphData.title);
+ });
+
+ it('contains graph widgets from slot', () => {
+ expect(areaChart.find({ ref: 'graphWidgets' }).text()).toBe(mockWidgets);
+ });
+
+ describe('methods', () => {
+ describe('formatTooltipText', () => {
+ const mockDate = mockNormalizedMetrics.queries[0].result[0].values[0].time;
+ const generateSeriesData = type => ({
+ seriesData: [
+ {
+ componentSubType: type,
+ value: [mockDate, 4],
+ },
+ ],
+ value: mockDate,
+ });
+
+ describe('series is of line type', () => {
+ beforeEach(() => {
+ areaChart.vm.formatTooltipText(generateSeriesData('line'));
+ });
+
+ it('formats tooltip title', () => {
+ expect(areaChart.vm.tooltipPopoverTitle).toBe('28 Feb 2019, 11:11AM');
+ });
+
+ it('formats tooltip content', () => {
+ expect(areaChart.vm.tooltipPopoverContent).toBe('Invocations (requests): 4');
+ });
+ });
+
+ it('verify default interval value of 1', () => {
+ expect(areaChart.vm.getInterval).toBe(1);
+ });
+ });
+
+ describe('onResize', () => {
+ const mockWidth = 233;
+
+ beforeEach(() => {
+ spyOn(Element.prototype, 'getBoundingClientRect').and.callFake(() => ({
+ width: mockWidth,
+ }));
+ areaChart.vm.onResize();
+ });
+
+ it('sets area chart width', () => {
+ expect(areaChart.vm.width).toBe(mockWidth);
+ });
+ });
+ });
+
+ describe('computed', () => {
+ describe('chartData', () => {
+ it('utilizes all data points', () => {
+ expect(Object.keys(areaChart.vm.chartData)).toEqual(['requests']);
+ expect(areaChart.vm.chartData.requests.length).toBe(2);
+ });
+
+ it('creates valid data', () => {
+ const data = areaChart.vm.chartData.requests;
+
+ expect(
+ data.filter(
+ datum => new Date(datum.time).getTime() > 0 && typeof datum.value === 'number',
+ ).length,
+ ).toBe(data.length);
+ });
+ });
+
+ describe('generateSeries', () => {
+ it('utilizes correct time data', () => {
+ expect(areaChart.vm.generateSeries.data).toEqual([
+ ['2019-02-28T11:11:38.756Z', 0],
+ ['2019-02-28T11:12:38.756Z', 0],
+ ]);
+ });
+ });
+
+ describe('xAxisLabel', () => {
+ it('constructs a label for the chart x-axis', () => {
+ expect(areaChart.vm.xAxisLabel).toBe('invocations / minute');
+ });
+ });
+
+ describe('yAxisLabel', () => {
+ it('constructs a label for the chart y-axis', () => {
+ expect(areaChart.vm.yAxisLabel).toBe('Invocations (requests)');
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/serverless/components/environment_row_spec.js b/spec/javascripts/serverless/components/environment_row_spec.js
index bdf7a714910..932d712dbec 100644
--- a/spec/javascripts/serverless/components/environment_row_spec.js
+++ b/spec/javascripts/serverless/components/environment_row_spec.js
@@ -1,81 +1,70 @@
-import Vue from 'vue';
-
import environmentRowComponent from '~/serverless/components/environment_row.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import ServerlessStore from '~/serverless/stores/serverless_store';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
import { mockServerlessFunctions, mockServerlessFunctionsDiffEnv } from '../mock_data';
+import { translate } from '~/serverless/utils';
-const createComponent = (env, envName) =>
- mountComponent(Vue.extend(environmentRowComponent), { env, envName });
+const createComponent = (localVue, env, envName) =>
+ shallowMount(environmentRowComponent, { localVue, propsData: { env, envName } }).vm;
describe('environment row component', () => {
describe('default global cluster case', () => {
+ let localVue;
let vm;
beforeEach(() => {
- const store = new ServerlessStore(false, '/cluster_path', 'help_path');
- store.updateFunctionsFromServer(mockServerlessFunctions);
- vm = createComponent(store.state.functions['*'], '*');
+ localVue = createLocalVue();
+ vm = createComponent(localVue, translate(mockServerlessFunctions)['*'], '*');
});
+ afterEach(() => vm.$destroy());
+
it('has the correct envId', () => {
expect(vm.envId).toEqual('env-global');
- vm.$destroy();
});
it('is open by default', () => {
expect(vm.isOpenClass).toEqual({ 'is-open': true });
- vm.$destroy();
});
it('generates correct output', () => {
- expect(vm.$el.querySelectorAll('li').length).toEqual(2);
expect(vm.$el.id).toEqual('env-global');
expect(vm.$el.classList.contains('is-open')).toBe(true);
expect(vm.$el.querySelector('div.title').innerHTML.trim()).toEqual('*');
-
- vm.$destroy();
});
it('opens and closes correctly', () => {
expect(vm.isOpen).toBe(true);
vm.toggleOpen();
- Vue.nextTick(() => {
- expect(vm.isOpen).toBe(false);
- });
- vm.$destroy();
+ expect(vm.isOpen).toBe(false);
});
});
describe('default named cluster case', () => {
let vm;
+ let localVue;
beforeEach(() => {
- const store = new ServerlessStore(false, '/cluster_path', 'help_path');
- store.updateFunctionsFromServer(mockServerlessFunctionsDiffEnv);
- vm = createComponent(store.state.functions.test, 'test');
+ localVue = createLocalVue();
+ vm = createComponent(localVue, translate(mockServerlessFunctionsDiffEnv).test, 'test');
});
+ afterEach(() => vm.$destroy());
+
it('has the correct envId', () => {
expect(vm.envId).toEqual('env-test');
- vm.$destroy();
});
it('is open by default', () => {
expect(vm.isOpenClass).toEqual({ 'is-open': true });
- vm.$destroy();
});
it('generates correct output', () => {
- expect(vm.$el.querySelectorAll('li').length).toEqual(1);
expect(vm.$el.id).toEqual('env-test');
expect(vm.$el.classList.contains('is-open')).toBe(true);
expect(vm.$el.querySelector('div.title').innerHTML.trim()).toEqual('test');
-
- vm.$destroy();
});
});
});
diff --git a/spec/javascripts/serverless/components/function_details_spec.js b/spec/javascripts/serverless/components/function_details_spec.js
new file mode 100644
index 00000000000..a29d4a296ef
--- /dev/null
+++ b/spec/javascripts/serverless/components/function_details_spec.js
@@ -0,0 +1,113 @@
+import Vuex from 'vuex';
+
+import functionDetailsComponent from '~/serverless/components/function_details.vue';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { createStore } from '~/serverless/store';
+
+describe('functionDetailsComponent', () => {
+ let localVue;
+ let component;
+ let store;
+
+ beforeEach(() => {
+ localVue = createLocalVue();
+ localVue.use(Vuex);
+
+ store = createStore();
+ });
+
+ afterEach(() => {
+ component.vm.$destroy();
+ });
+
+ describe('Verify base functionality', () => {
+ const serviceStub = {
+ name: 'test',
+ description: 'a description',
+ environment: '*',
+ url: 'http://service.com/test',
+ namespace: 'test-ns',
+ podcount: 0,
+ metricsUrl: '/metrics',
+ };
+
+ it('has a name, description, URL, and no pods loaded', () => {
+ component = shallowMount(functionDetailsComponent, {
+ localVue,
+ store,
+ propsData: {
+ func: serviceStub,
+ hasPrometheus: false,
+ clustersPath: '/clusters',
+ helpPath: '/help',
+ },
+ });
+
+ expect(
+ component.vm.$el.querySelector('.serverless-function-name').innerHTML.trim(),
+ ).toContain('test');
+
+ expect(
+ component.vm.$el.querySelector('.serverless-function-description').innerHTML.trim(),
+ ).toContain('a description');
+
+ expect(component.vm.$el.querySelector('p').innerHTML.trim()).toContain(
+ 'No pods loaded at this time.',
+ );
+ });
+
+ it('has a pods loaded', () => {
+ serviceStub.podcount = 1;
+
+ component = shallowMount(functionDetailsComponent, {
+ localVue,
+ store,
+ propsData: {
+ func: serviceStub,
+ hasPrometheus: false,
+ clustersPath: '/clusters',
+ helpPath: '/help',
+ },
+ });
+
+ expect(component.vm.$el.querySelector('p').innerHTML.trim()).toContain('1 pod in use');
+ });
+
+ it('has multiple pods loaded', () => {
+ serviceStub.podcount = 3;
+
+ component = shallowMount(functionDetailsComponent, {
+ localVue,
+ store,
+ propsData: {
+ func: serviceStub,
+ hasPrometheus: false,
+ clustersPath: '/clusters',
+ helpPath: '/help',
+ },
+ });
+
+ expect(component.vm.$el.querySelector('p').innerHTML.trim()).toContain('3 pods in use');
+ });
+
+ it('can support a missing description', () => {
+ serviceStub.description = null;
+
+ component = shallowMount(functionDetailsComponent, {
+ localVue,
+ store,
+ propsData: {
+ func: serviceStub,
+ hasPrometheus: false,
+ clustersPath: '/clusters',
+ helpPath: '/help',
+ },
+ });
+
+ expect(
+ component.vm.$el.querySelector('.serverless-function-description').querySelector('div')
+ .innerHTML.length,
+ ).toEqual(0);
+ });
+ });
+});
diff --git a/spec/javascripts/serverless/components/function_row_spec.js b/spec/javascripts/serverless/components/function_row_spec.js
index 6933a8f6c87..3987e1753bd 100644
--- a/spec/javascripts/serverless/components/function_row_spec.js
+++ b/spec/javascripts/serverless/components/function_row_spec.js
@@ -1,11 +1,9 @@
-import Vue from 'vue';
-
import functionRowComponent from '~/serverless/components/function_row.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { shallowMount } from '@vue/test-utils';
import { mockServerlessFunction } from '../mock_data';
-const createComponent = func => mountComponent(Vue.extend(functionRowComponent), { func });
+const createComponent = func => shallowMount(functionRowComponent, { propsData: { func } }).vm;
describe('functionRowComponent', () => {
it('Parses the function details correctly', () => {
@@ -13,10 +11,7 @@ describe('functionRowComponent', () => {
expect(vm.$el.querySelector('b').innerHTML).toEqual(mockServerlessFunction.name);
expect(vm.$el.querySelector('span').innerHTML).toEqual(mockServerlessFunction.image);
- expect(vm.$el.querySelector('time').getAttribute('data-original-title')).not.toBe(null);
- expect(vm.$el.querySelector('div.url-text-field').innerHTML).toEqual(
- mockServerlessFunction.url,
- );
+ expect(vm.$el.querySelector('timeago-stub').getAttribute('time')).not.toBe(null);
vm.$destroy();
});
@@ -25,8 +20,6 @@ describe('functionRowComponent', () => {
const vm = createComponent(mockServerlessFunction);
expect(vm.checkClass(vm.$el.querySelector('p'))).toBe(true); // check somewhere inside the row
- expect(vm.checkClass(vm.$el.querySelector('svg'))).toBe(false); // check a button image
- expect(vm.checkClass(vm.$el.querySelector('div.url-text-field'))).toBe(false); // check the url bar
vm.$destroy();
});
diff --git a/spec/javascripts/serverless/components/functions_spec.js b/spec/javascripts/serverless/components/functions_spec.js
index 85cfe71281f..c32978ea58a 100644
--- a/spec/javascripts/serverless/components/functions_spec.js
+++ b/spec/javascripts/serverless/components/functions_spec.js
@@ -1,68 +1,101 @@
-import Vue from 'vue';
+import Vuex from 'vuex';
import functionsComponent from '~/serverless/components/functions.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-import ServerlessStore from '~/serverless/stores/serverless_store';
-
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { createStore } from '~/serverless/store';
import { mockServerlessFunctions } from '../mock_data';
-const createComponent = (
- functions,
- installed = true,
- loadingData = true,
- hasFunctionData = true,
-) => {
- const component = Vue.extend(functionsComponent);
+describe('functionsComponent', () => {
+ let component;
+ let store;
+ let localVue;
+
+ beforeEach(() => {
+ localVue = createLocalVue();
+ localVue.use(Vuex);
- return mountComponent(component, {
- functions,
- installed,
- clustersPath: '/testClusterPath',
- helpPath: '/helpPath',
- loadingData,
- hasFunctionData,
+ store = createStore();
});
-};
-describe('functionsComponent', () => {
- it('should render empty state when Knative is not installed', () => {
- const vm = createComponent({}, false);
+ afterEach(() => {
+ component.vm.$destroy();
+ });
- expect(vm.$el.querySelector('div.row').classList.contains('js-empty-state')).toBe(true);
- expect(vm.$el.querySelector('h4.state-title').innerHTML.trim()).toEqual(
- 'Getting started with serverless',
- );
+ it('should render empty state when Knative is not installed', () => {
+ component = shallowMount(functionsComponent, {
+ localVue,
+ store,
+ propsData: {
+ installed: false,
+ clustersPath: '',
+ helpPath: '',
+ statusPath: '',
+ },
+ sync: false,
+ });
- vm.$destroy();
+ expect(component.vm.$el.querySelector('emptystate-stub')).not.toBe(null);
});
it('should render a loading component', () => {
- const vm = createComponent({});
+ store.dispatch('requestFunctionsLoading');
+ component = shallowMount(functionsComponent, {
+ localVue,
+ store,
+ propsData: {
+ installed: true,
+ clustersPath: '',
+ helpPath: '',
+ statusPath: '',
+ },
+ sync: false,
+ });
- expect(vm.$el.querySelector('.gl-responsive-table-row')).not.toBe(null);
- expect(vm.$el.querySelector('div.animation-container')).not.toBe(null);
+ expect(component.vm.$el.querySelector('glloadingicon-stub')).not.toBe(null);
});
it('should render empty state when there is no function data', () => {
- const vm = createComponent({}, true, false, false);
+ store.dispatch('receiveFunctionsNoDataSuccess');
+ component = shallowMount(functionsComponent, {
+ localVue,
+ store,
+ propsData: {
+ installed: true,
+ clustersPath: '',
+ helpPath: '',
+ statusPath: '',
+ },
+ sync: false,
+ });
expect(
- vm.$el.querySelector('.empty-state, .js-empty-state').classList.contains('js-empty-state'),
+ component.vm.$el
+ .querySelector('.empty-state, .js-empty-state')
+ .classList.contains('js-empty-state'),
).toBe(true);
- expect(vm.$el.querySelector('h4.state-title').innerHTML.trim()).toEqual(
+ expect(component.vm.$el.querySelector('.state-title, .text-center').innerHTML.trim()).toEqual(
'No functions available',
);
-
- vm.$destroy();
});
it('should render the functions list', () => {
- const store = new ServerlessStore(false, '/cluster_path', 'help_path');
- store.updateFunctionsFromServer(mockServerlessFunctions);
- const vm = createComponent(store.state.functions, true, false);
+ component = shallowMount(functionsComponent, {
+ localVue,
+ store,
+ propsData: {
+ installed: true,
+ clustersPath: '',
+ helpPath: '',
+ statusPath: '',
+ },
+ sync: false,
+ });
+
+ component.vm.$store.dispatch('receiveFunctionsSuccess', mockServerlessFunctions);
- expect(vm.$el.querySelector('div.groups-list-tree-container')).not.toBe(null);
- expect(vm.$el.querySelector('#env-global').classList.contains('has-children')).toBe(true);
+ return component.vm.$nextTick().then(() => {
+ expect(component.vm.$el.querySelector('environmentrow-stub')).not.toBe(null);
+ });
});
});
diff --git a/spec/javascripts/serverless/components/missing_prometheus_spec.js b/spec/javascripts/serverless/components/missing_prometheus_spec.js
new file mode 100644
index 00000000000..77aca03772b
--- /dev/null
+++ b/spec/javascripts/serverless/components/missing_prometheus_spec.js
@@ -0,0 +1,37 @@
+import missingPrometheusComponent from '~/serverless/components/missing_prometheus.vue';
+import { shallowMount } from '@vue/test-utils';
+
+const createComponent = missingData =>
+ shallowMount(missingPrometheusComponent, {
+ propsData: {
+ clustersPath: '/clusters',
+ helpPath: '/help',
+ missingData,
+ },
+ }).vm;
+
+describe('missingPrometheusComponent', () => {
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render missing prometheus message', () => {
+ vm = createComponent(false);
+
+ expect(vm.$el.querySelector('.state-description').innerHTML.trim()).toContain(
+ 'Function invocation metrics require Prometheus to be installed first.',
+ );
+
+ expect(vm.$el.querySelector('glbutton-stub').getAttribute('variant')).toEqual('success');
+ });
+
+ it('should render no prometheus data message', () => {
+ vm = createComponent(true);
+
+ expect(vm.$el.querySelector('.state-description').innerHTML.trim()).toContain(
+ 'Invocation metrics loading or not available at this time.',
+ );
+ });
+});
diff --git a/spec/javascripts/serverless/components/pod_box_spec.js b/spec/javascripts/serverless/components/pod_box_spec.js
new file mode 100644
index 00000000000..69ac1a2bb5f
--- /dev/null
+++ b/spec/javascripts/serverless/components/pod_box_spec.js
@@ -0,0 +1,22 @@
+import podBoxComponent from '~/serverless/components/pod_box.vue';
+import { shallowMount } from '@vue/test-utils';
+
+const createComponent = count =>
+ shallowMount(podBoxComponent, {
+ propsData: {
+ count,
+ },
+ }).vm;
+
+describe('podBoxComponent', () => {
+ it('should render three boxes', () => {
+ const count = 3;
+ const vm = createComponent(count);
+ const rects = vm.$el.querySelectorAll('rect');
+
+ expect(rects.length).toEqual(3);
+ expect(parseInt(rects[2].getAttribute('x'), 10)).toEqual(40);
+
+ vm.$destroy();
+ });
+});
diff --git a/spec/javascripts/serverless/components/url_spec.js b/spec/javascripts/serverless/components/url_spec.js
index 21a879a49bb..08c3e4146b1 100644
--- a/spec/javascripts/serverless/components/url_spec.js
+++ b/spec/javascripts/serverless/components/url_spec.js
@@ -1,15 +1,13 @@
import Vue from 'vue';
-
import urlComponent from '~/serverless/components/url.vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
-
-const createComponent = uri => {
- const component = Vue.extend(urlComponent);
+import { shallowMount } from '@vue/test-utils';
- return mountComponent(component, {
- uri,
- });
-};
+const createComponent = uri =>
+ shallowMount(Vue.extend(urlComponent), {
+ propsData: {
+ uri,
+ },
+ }).vm;
describe('urlComponent', () => {
it('should render correctly', () => {
@@ -17,9 +15,7 @@ describe('urlComponent', () => {
const vm = createComponent(uri);
expect(vm.$el.classList.contains('clipboard-group')).toBe(true);
- expect(vm.$el.querySelector('.js-clipboard-btn').getAttribute('data-clipboard-text')).toEqual(
- uri,
- );
+ expect(vm.$el.querySelector('clipboardbutton-stub').getAttribute('text')).toEqual(uri);
expect(vm.$el.querySelector('.url-text-field').innerHTML).toEqual(uri);
diff --git a/spec/javascripts/serverless/mock_data.js b/spec/javascripts/serverless/mock_data.js
index ecd393b174c..a2c18616324 100644
--- a/spec/javascripts/serverless/mock_data.js
+++ b/spec/javascripts/serverless/mock_data.js
@@ -77,3 +77,60 @@ export const mockMultilineServerlessFunction = {
description: 'testfunc1\nA test service line\\nWith additional services',
image: 'knative-test-container-buildtemplate',
};
+
+export const mockMetrics = {
+ success: true,
+ last_update: '2019-02-28T19:11:38.926Z',
+ metrics: {
+ id: 22,
+ title: 'Knative function invocations',
+ required_metrics: ['container_memory_usage_bytes', 'container_cpu_usage_seconds_total'],
+ weight: 0,
+ y_label: 'Invocations',
+ queries: [
+ {
+ query_range:
+ 'floor(sum(rate(istio_revision_request_count{destination_configuration="%{function_name}", destination_namespace="%{kube_namespace}"}[1m])*30))',
+ unit: 'requests',
+ label: 'invocations / minute',
+ result: [
+ {
+ metric: {},
+ values: [[1551352298.756, '0'], [1551352358.756, '0']],
+ },
+ ],
+ },
+ ],
+ },
+};
+
+export const mockNormalizedMetrics = {
+ id: 22,
+ title: 'Knative function invocations',
+ required_metrics: ['container_memory_usage_bytes', 'container_cpu_usage_seconds_total'],
+ weight: 0,
+ y_label: 'Invocations',
+ queries: [
+ {
+ query_range:
+ 'floor(sum(rate(istio_revision_request_count{destination_configuration="%{function_name}", destination_namespace="%{kube_namespace}"}[1m])*30))',
+ unit: 'requests',
+ label: 'invocations / minute',
+ result: [
+ {
+ metric: {},
+ values: [
+ {
+ time: '2019-02-28T11:11:38.756Z',
+ value: 0,
+ },
+ {
+ time: '2019-02-28T11:12:38.756Z',
+ value: 0,
+ },
+ ],
+ },
+ ],
+ },
+ ],
+};
diff --git a/spec/javascripts/serverless/store/actions_spec.js b/spec/javascripts/serverless/store/actions_spec.js
new file mode 100644
index 00000000000..602798573e9
--- /dev/null
+++ b/spec/javascripts/serverless/store/actions_spec.js
@@ -0,0 +1,88 @@
+import MockAdapter from 'axios-mock-adapter';
+import statusCodes from '~/lib/utils/http_status';
+import { fetchFunctions, fetchMetrics } from '~/serverless/store/actions';
+import { mockServerlessFunctions, mockMetrics } from '../mock_data';
+import axios from '~/lib/utils/axios_utils';
+import testAction from '../../helpers/vuex_action_helper';
+import { adjustMetricQuery } from '../utils';
+
+describe('ServerlessActions', () => {
+ describe('fetchFunctions', () => {
+ it('should successfully fetch functions', done => {
+ const endpoint = '/functions';
+ const mock = new MockAdapter(axios);
+ mock.onGet(endpoint).reply(statusCodes.OK, JSON.stringify(mockServerlessFunctions));
+
+ testAction(
+ fetchFunctions,
+ { functionsPath: endpoint },
+ {},
+ [],
+ [
+ { type: 'requestFunctionsLoading' },
+ { type: 'receiveFunctionsSuccess', payload: mockServerlessFunctions },
+ ],
+ () => {
+ mock.restore();
+ done();
+ },
+ );
+ });
+
+ it('should successfully retry', done => {
+ const endpoint = '/functions';
+ const mock = new MockAdapter(axios);
+ mock.onGet(endpoint).reply(statusCodes.NO_CONTENT);
+
+ testAction(
+ fetchFunctions,
+ { functionsPath: endpoint },
+ {},
+ [],
+ [{ type: 'requestFunctionsLoading' }],
+ () => {
+ mock.restore();
+ done();
+ },
+ );
+ });
+ });
+
+ describe('fetchMetrics', () => {
+ it('should return no prometheus', done => {
+ const endpoint = '/metrics';
+ const mock = new MockAdapter(axios);
+ mock.onGet(endpoint).reply(statusCodes.NO_CONTENT);
+
+ testAction(
+ fetchMetrics,
+ { metricsPath: endpoint, hasPrometheus: false },
+ {},
+ [],
+ [{ type: 'receiveMetricsNoPrometheus' }],
+ () => {
+ mock.restore();
+ done();
+ },
+ );
+ });
+
+ it('should successfully fetch metrics', done => {
+ const endpoint = '/metrics';
+ const mock = new MockAdapter(axios);
+ mock.onGet(endpoint).reply(statusCodes.OK, JSON.stringify(mockMetrics));
+
+ testAction(
+ fetchMetrics,
+ { metricsPath: endpoint, hasPrometheus: true },
+ {},
+ [],
+ [{ type: 'receiveMetricsSuccess', payload: adjustMetricQuery(mockMetrics) }],
+ () => {
+ mock.restore();
+ done();
+ },
+ );
+ });
+ });
+});
diff --git a/spec/javascripts/serverless/store/getters_spec.js b/spec/javascripts/serverless/store/getters_spec.js
new file mode 100644
index 00000000000..fb549c8f153
--- /dev/null
+++ b/spec/javascripts/serverless/store/getters_spec.js
@@ -0,0 +1,43 @@
+import serverlessState from '~/serverless/store/state';
+import * as getters from '~/serverless/store/getters';
+import { mockServerlessFunctions } from '../mock_data';
+
+describe('Serverless Store Getters', () => {
+ let state;
+
+ beforeEach(() => {
+ state = serverlessState;
+ });
+
+ describe('hasPrometheusMissingData', () => {
+ it('should return false if Prometheus is not installed', () => {
+ state.hasPrometheus = false;
+
+ expect(getters.hasPrometheusMissingData(state)).toEqual(false);
+ });
+
+ it('should return false if Prometheus is installed and there is data', () => {
+ state.hasPrometheusData = true;
+
+ expect(getters.hasPrometheusMissingData(state)).toEqual(false);
+ });
+
+ it('should return true if Prometheus is installed and there is no data', () => {
+ state.hasPrometheus = true;
+ state.hasPrometheusData = false;
+
+ expect(getters.hasPrometheusMissingData(state)).toEqual(true);
+ });
+ });
+
+ describe('getFunctions', () => {
+ it('should translate the raw function array to group the functions per environment scope', () => {
+ state.functions = mockServerlessFunctions;
+
+ const funcs = getters.getFunctions(state);
+
+ expect(Object.keys(funcs)).toContain('*');
+ expect(funcs['*'].length).toEqual(2);
+ });
+ });
+});
diff --git a/spec/javascripts/serverless/store/mutations_spec.js b/spec/javascripts/serverless/store/mutations_spec.js
new file mode 100644
index 00000000000..ca3053e5c38
--- /dev/null
+++ b/spec/javascripts/serverless/store/mutations_spec.js
@@ -0,0 +1,86 @@
+import mutations from '~/serverless/store/mutations';
+import * as types from '~/serverless/store/mutation_types';
+import { mockServerlessFunctions, mockMetrics } from '../mock_data';
+
+describe('ServerlessMutations', () => {
+ describe('Functions List Mutations', () => {
+ it('should ensure loading is true', () => {
+ const state = {};
+
+ mutations[types.REQUEST_FUNCTIONS_LOADING](state);
+
+ expect(state.isLoading).toEqual(true);
+ });
+
+ it('should set proper state once functions are loaded', () => {
+ const state = {};
+
+ mutations[types.RECEIVE_FUNCTIONS_SUCCESS](state, mockServerlessFunctions);
+
+ expect(state.isLoading).toEqual(false);
+ expect(state.hasFunctionData).toEqual(true);
+ expect(state.functions).toEqual(mockServerlessFunctions);
+ });
+
+ it('should ensure loading has stopped and hasFunctionData is false when there are no functions available', () => {
+ const state = {};
+
+ mutations[types.RECEIVE_FUNCTIONS_NODATA_SUCCESS](state);
+
+ expect(state.isLoading).toEqual(false);
+ expect(state.hasFunctionData).toEqual(false);
+ expect(state.functions).toBe(undefined);
+ });
+
+ it('should ensure loading has stopped, and an error is raised', () => {
+ const state = {};
+
+ mutations[types.RECEIVE_FUNCTIONS_ERROR](state, 'sample error');
+
+ expect(state.isLoading).toEqual(false);
+ expect(state.hasFunctionData).toEqual(false);
+ expect(state.functions).toBe(undefined);
+ expect(state.error).not.toBe(undefined);
+ });
+ });
+
+ describe('Function Details Metrics Mutations', () => {
+ it('should ensure isLoading and hasPrometheus data flags indicate data is loaded', () => {
+ const state = {};
+
+ mutations[types.RECEIVE_METRICS_SUCCESS](state, mockMetrics);
+
+ expect(state.isLoading).toEqual(false);
+ expect(state.hasPrometheusData).toEqual(true);
+ expect(state.graphData).toEqual(mockMetrics);
+ });
+
+ it('should ensure isLoading and hasPrometheus data flags are cleared indicating no functions available', () => {
+ const state = {};
+
+ mutations[types.RECEIVE_METRICS_NODATA_SUCCESS](state);
+
+ expect(state.isLoading).toEqual(false);
+ expect(state.hasPrometheusData).toEqual(false);
+ expect(state.graphData).toBe(undefined);
+ });
+
+ it('should properly indicate an error', () => {
+ const state = {};
+
+ mutations[types.RECEIVE_METRICS_ERROR](state, 'sample error');
+
+ expect(state.hasPrometheusData).toEqual(false);
+ expect(state.error).not.toBe(undefined);
+ });
+
+ it('should properly indicate when prometheus is installed', () => {
+ const state = {};
+
+ mutations[types.RECEIVE_METRICS_NO_PROMETHEUS](state);
+
+ expect(state.hasPrometheus).toEqual(false);
+ expect(state.hasPrometheusData).toEqual(false);
+ });
+ });
+});
diff --git a/spec/javascripts/serverless/stores/serverless_store_spec.js b/spec/javascripts/serverless/stores/serverless_store_spec.js
deleted file mode 100644
index 72fd903d7d1..00000000000
--- a/spec/javascripts/serverless/stores/serverless_store_spec.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import ServerlessStore from '~/serverless/stores/serverless_store';
-import { mockServerlessFunctions, mockServerlessFunctionsDiffEnv } from '../mock_data';
-
-describe('Serverless Functions Store', () => {
- let store;
-
- beforeEach(() => {
- store = new ServerlessStore(false, '/cluster_path', 'help_path');
- });
-
- describe('#updateFunctionsFromServer', () => {
- it('should pass an empty hash object', () => {
- store.updateFunctionsFromServer();
-
- expect(store.state.functions).toEqual({});
- });
-
- it('should group functions to one global environment', () => {
- const mockServerlessData = mockServerlessFunctions;
- store.updateFunctionsFromServer(mockServerlessData);
-
- expect(Object.keys(store.state.functions)).toEqual(jasmine.objectContaining(['*']));
- expect(store.state.functions['*'].length).toEqual(2);
- });
-
- it('should group functions to multiple environments', () => {
- const mockServerlessData = mockServerlessFunctionsDiffEnv;
- store.updateFunctionsFromServer(mockServerlessData);
-
- expect(Object.keys(store.state.functions)).toEqual(jasmine.objectContaining(['*']));
- expect(store.state.functions['*'].length).toEqual(1);
- expect(store.state.functions.test.length).toEqual(1);
- expect(store.state.functions.test[0].name).toEqual('testfunc2');
- });
- });
-});
diff --git a/spec/javascripts/serverless/utils.js b/spec/javascripts/serverless/utils.js
new file mode 100644
index 00000000000..5ce2e37d493
--- /dev/null
+++ b/spec/javascripts/serverless/utils.js
@@ -0,0 +1,20 @@
+export const adjustMetricQuery = data => {
+ const updatedMetric = data.metrics;
+
+ const queries = data.metrics.queries.map(query => ({
+ ...query,
+ result: query.result.map(result => ({
+ ...result,
+ values: result.values.map(([timestamp, value]) => ({
+ time: new Date(timestamp * 1000).toISOString(),
+ value: Number(value),
+ })),
+ })),
+ }));
+
+ updatedMetric.queries = queries;
+ return updatedMetric;
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb b/spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb
new file mode 100644
index 00000000000..7f6283715f2
--- /dev/null
+++ b/spec/lib/gitlab/prometheus/queries/knative_invocation_query_spec.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Prometheus::Queries::KnativeInvocationQuery do
+ include PrometheusHelpers
+
+ let(:project) { create(:project) }
+ let(:serverless_func) { Serverless::Function.new(project, 'test-name', 'test-ns') }
+
+ let(:client) { double('prometheus_client') }
+ subject { described_class.new(client) }
+
+ context 'verify queries' do
+ before do
+ allow(PrometheusMetric).to receive(:find_by_identifier).and_return(create(:prometheus_metric, query: prometheus_istio_query('test-name', 'test-ns')))
+ allow(client).to receive(:query_range)
+ end
+
+ it 'has the query, but no data' do
+ results = subject.query(serverless_func.id)
+
+ expect(results.queries[0][:query_range]).to eql('floor(sum(rate(istio_revision_request_count{destination_configuration="test-name", destination_namespace="test-ns"}[1m])*30))')
+ end
+ end
+end
diff --git a/spec/models/serverless/function_spec.rb b/spec/models/serverless/function_spec.rb
new file mode 100644
index 00000000000..1854d5f9415
--- /dev/null
+++ b/spec/models/serverless/function_spec.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Serverless::Function do
+ let(:project) { create(:project) }
+ let(:func) { described_class.new(project, 'test', 'test-ns') }
+
+ it 'has a proper id' do
+ expect(func.id).to eql("#{project.id}/test/test-ns")
+ expect(func.name).to eql("test")
+ expect(func.namespace).to eql("test-ns")
+ end
+
+ it 'can decode an identifier' do
+ f = described_class.find_by_id("#{project.id}/testfunc/dummy-ns")
+
+ expect(f.name).to eql("testfunc")
+ expect(f.namespace).to eql("dummy-ns")
+ end
+end
diff --git a/spec/support/helpers/prometheus_helpers.rb b/spec/support/helpers/prometheus_helpers.rb
index 08d1d7a6059..87f825152cf 100644
--- a/spec/support/helpers/prometheus_helpers.rb
+++ b/spec/support/helpers/prometheus_helpers.rb
@@ -7,6 +7,10 @@ module PrometheusHelpers
%{avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="#{environment_slug}"}[2m])) * 100}
end
+ def prometheus_istio_query(function_name, kube_namespace)
+ %{floor(sum(rate(istio_revision_request_count{destination_configuration=\"#{function_name}\", destination_namespace=\"#{kube_namespace}\"}[1m])*30))}
+ end
+
def prometheus_ping_url(prometheus_query)
query = { query: prometheus_query }.to_query