Commit 3b291822 authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'remove-ff-job-log-json' into 'master'

Remove FF job_log_json flag from BE and FE

See merge request gitlab-org/gitlab!34538
parents 8812a456 9ad5bb11
......@@ -17,7 +17,7 @@ import UnmetPrerequisitesBlock from './unmet_prerequisites_block.vue';
import Sidebar from './sidebar.vue';
import { sprintf } from '~/locale';
import delayedJobMixin from '../mixins/delayed_job_mixin';
import { isNewJobLogActive } from '../store/utils';
import Log from './log/log.vue';
export default {
name: 'JobPageApp',
......@@ -28,7 +28,7 @@ export default {
EnvironmentsBlock,
ErasedBlock,
Icon,
Log: () => (isNewJobLogActive() ? import('./log/log.vue') : import('./job_log.vue')),
Log,
LogTopBar,
StuckBlock,
UnmetPrerequisitesBlock,
......
<script>
import { mapState, mapActions } from 'vuex';
export default {
name: 'JobLog',
props: {
trace: {
type: String,
required: true,
},
isComplete: {
type: Boolean,
required: true,
},
},
computed: {
...mapState(['isScrolledToBottomBeforeReceivingTrace']),
},
updated() {
this.$nextTick(() => {
this.handleScrollDown();
});
},
mounted() {
this.$nextTick(() => {
this.handleScrollDown();
});
},
methods: {
...mapActions(['scrollBottom']),
/**
* The job log is sent in HTML, which means we need to use `v-html` to render it
* Using the updated hook with $nextTick is not enough to wait for the DOM to be updated
* in this case because it runs before `v-html` has finished running, since there's no
* Vue binding.
* In order to scroll the page down after `v-html` has finished, we need to use setTimeout
*/
handleScrollDown() {
if (this.isScrolledToBottomBeforeReceivingTrace) {
setTimeout(() => {
this.scrollBottom();
}, 0);
}
},
},
};
</script>
<template>
<pre class="js-build-trace build-trace qa-build-trace">
<code class="bash" v-html="trace">
</code>
<div v-if="!isComplete" class="js-log-animation build-loader-animation">
<div class="dot"></div>
<div class="dot"></div>
<div class="dot"></div>
</div>
</pre>
</template>
import Vue from 'vue';
import * as types from './mutation_types';
import { logLinesParser, updateIncrementalTrace, isNewJobLogActive } from './utils';
import { logLinesParser, updateIncrementalTrace } from './utils';
export default {
[types.SET_JOB_ENDPOINT](state, endpoint) {
......@@ -25,22 +25,16 @@ export default {
}
if (log.append) {
if (isNewJobLogActive()) {
state.trace = log.lines ? updateIncrementalTrace(log.lines, state.trace) : state.trace;
} else {
state.trace += log.html;
}
state.trace = log.lines ? updateIncrementalTrace(log.lines, state.trace) : state.trace;
state.traceSize += log.size;
} else {
// When the job still does not have a trace
// the trace response will not have a defined
// html or size. We keep the old value otherwise these
// will be set to `null`
if (isNewJobLogActive()) {
state.trace = log.lines ? logLinesParser(log.lines) : state.trace;
} else {
state.trace = log.html || state.trace;
}
state.trace = log.lines ? logLinesParser(log.lines) : state.trace;
state.traceSize = log.size || state.traceSize;
}
......
import { isNewJobLogActive } from './utils';
export default () => ({
jobEndpoint: null,
traceEndpoint: null,
......@@ -18,7 +16,7 @@ export default () => ({
// Used to check if we should keep the automatic scroll
isScrolledToBottomBeforeReceivingTrace: true,
trace: isNewJobLogActive() ? [] : '',
trace: [],
isTraceComplete: false,
traceSize: 0,
isTraceSizeVisible: false,
......
......@@ -177,5 +177,3 @@ export const updateIncrementalTrace = (newLog = [], oldParsed = []) => {
return logLinesParser(newLog, parsedLog);
};
export const isNewJobLogActive = () => gon && gon.features && gon.features.jobLogJson;
......@@ -11,9 +11,6 @@ class Projects::JobsController < Projects::ApplicationController
before_action :authorize_erase_build!, only: [:erase]
before_action :authorize_use_build_terminal!, only: [:terminal, :terminal_websocket_authorize]
before_action :verify_api_request!, only: :terminal_websocket_authorize
before_action only: [:show] do
push_frontend_feature_flag(:job_log_json, project, default_enabled: true)
end
before_action :authorize_create_proxy_build!, only: :proxy_websocket_authorize
before_action :verify_proxy_request!, only: :proxy_websocket_authorize
......@@ -55,15 +52,10 @@ class Projects::JobsController < Projects::ApplicationController
format.json do
build.trace.being_watched!
# TODO: when the feature flag is removed we should not pass
# content_format to serialize method.
content_format = Feature.enabled?(:job_log_json, @project, default_enabled: true) ? :json : :html
build_trace = Ci::BuildTrace.new(
build: @build,
stream: stream,
state: params[:state],
content_format: content_format)
state: params[:state])
render json: BuildTraceSerializer
.new(project: @project, current_user: @current_user)
......
......@@ -2,40 +2,22 @@
module Ci
class BuildTrace
CONVERTERS = {
html: Gitlab::Ci::Ansi2html,
json: Gitlab::Ci::Ansi2json
}.freeze
attr_reader :trace, :build
delegate :state, :append, :truncated, :offset, :size, :total, to: :trace, allow_nil: true
delegate :id, :status, :complete?, to: :build, prefix: true
def initialize(build:, stream:, state:, content_format:)
def initialize(build:, stream:, state:)
@build = build
@content_format = content_format
if stream.valid?
stream.limit
@trace = CONVERTERS.fetch(content_format).convert(stream.stream, state)
@trace = Gitlab::Ci::Ansi2json.convert(stream.stream, state)
end
end
def json?
@content_format == :json
end
def html?
@content_format == :html
end
def json_lines
@trace&.lines if json?
end
def html_lines
@trace&.html if html?
def lines
@trace&.lines
end
end
end
......@@ -12,6 +12,5 @@ class BuildTraceEntity < Grape::Entity
expose :size
expose :total
expose :json_lines, as: :lines, if: ->(*) { object.json? }
expose :html_lines, as: :html, if: ->(*) { object.html? }
expose :lines
end
---
title: Remove legacy job log rendering
merge_request: 34538
author:
type: other
......@@ -13,8 +13,6 @@ module QA
after do
@runner.remove_via_api! if @runner
Runtime::Feature.enable('job_log_json') if @job_log_json_flag_enabled
end
before do
......
......@@ -13,8 +13,6 @@ module QA
after(:all) do
@runner.remove_via_api!
Runtime::Feature.enable('job_log_json') if @job_log_json_flag_enabled
end
before(:all) do
......
......@@ -646,109 +646,6 @@ RSpec.describe Projects::JobsController, :clean_gitlab_redis_shared_state do
end
end
describe 'GET legacy trace.json' do
before do
stub_feature_flags(job_log_json: false)
get_trace
end
context 'when job has a trace artifact' do
let(:job) { create(:ci_build, :trace_artifact, pipeline: pipeline) }
it 'returns a trace' do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq job.id
expect(json_response['status']).to eq job.status
expect(json_response['state']).to be_present
expect(json_response['append']).not_to be_nil
expect(json_response['truncated']).not_to be_nil
expect(json_response['size']).to be_present
expect(json_response['total']).to be_present
expect(json_response['html']).to eq(job.trace.html)
end
end
context 'when job has a trace' do
let(:job) { create(:ci_build, :trace_live, pipeline: pipeline) }
it 'returns a trace' do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq job.id
expect(json_response['status']).to eq job.status
expect(json_response['html']).to eq('<span>BUILD TRACE</span>')
end
end
context 'when job has no traces' do
let(:job) { create(:ci_build, pipeline: pipeline) }
it 'returns no traces' do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq job.id
expect(json_response['status']).to eq job.status
expect(json_response['html']).to be_nil
end
end
context 'when job has a trace with ANSI sequence and Unicode' do
let(:job) { create(:ci_build, :unicode_trace_live, pipeline: pipeline) }
it 'returns a trace with Unicode' do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq job.id
expect(json_response['status']).to eq job.status
expect(json_response['html']).to include("ヾ(´༎ຶД༎ຶ`)ノ")
end
end
context 'when trace artifact is in ObjectStorage' do
let(:url) { 'http://object-storage/trace' }
let(:file_path) { expand_fixture_path('trace/sample_trace') }
let!(:job) { create(:ci_build, :success, :trace_artifact, pipeline: pipeline) }
before do
allow_any_instance_of(JobArtifactUploader).to receive(:file_storage?) { false }
allow_any_instance_of(JobArtifactUploader).to receive(:url) { url }
allow_any_instance_of(JobArtifactUploader).to receive(:size) { File.size(file_path) }
end
context 'when there are no network issues' do
before do
stub_remote_url_206(url, file_path)
get_trace
end
it 'returns a trace' do
expect(response).to have_gitlab_http_status(:ok)
expect(json_response['id']).to eq job.id
expect(json_response['status']).to eq job.status
expect(json_response['html']).to eq(job.trace.html)
end
end
context 'when there is a network issue' do
before do
stub_remote_url_500(url)
end
it 'returns a trace' do
expect { get_trace }.to raise_error(Gitlab::HttpIO::FailedToGetChunkError)
end
end
end
def get_trace
get :trace,
params: {
namespace_id: project.namespace,
project_id: project,
id: job.id
},
format: :json
end
end
describe 'GET status.json' do
let(:job) { create(:ci_build, pipeline: pipeline) }
let(:status) { job.detailed_status(double('user')) }
......
......@@ -837,7 +837,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do
it 'renders empty state' do
expect(page).to have_content(job.detailed_status(user).illustration[:title])
expect(page).not_to have_selector('.js-build-trace')
expect(page).not_to have_selector('.job-log')
expect(page).to have_content('This job has been canceled')
end
end
......@@ -852,7 +852,7 @@ RSpec.describe 'Jobs', :clean_gitlab_redis_shared_state do
it 'renders empty state' do
expect(page).to have_content(job.detailed_status(user).illustration[:title])
expect(page).not_to have_selector('.js-build-trace')
expect(page).not_to have_selector('.job-log')
expect(page).to have_content('This job has been skipped')
end
end
......
......@@ -397,132 +397,31 @@ describe('Job App', () => {
});
});
describe('trace output', () => {
describe('with append flag', () => {
it('appends the log content to the existing one', () =>
setupAndMount({
traceData: {
html: '<span>More<span>',
status: 'running',
state: 'newstate',
append: true,
complete: true,
},
})
.then(() => {
store.state.trace = 'Update';
return wrapper.vm.$nextTick();
})
.then(() => {
expect(
wrapper
.find('.js-build-trace')
.text()
.trim(),
).toEqual('Update');
}));
describe('trace controls', () => {
beforeEach(() =>
setupAndMount({
traceData: {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
complete: true,
},
}),
);
it('should render scroll buttons', () => {
expect(wrapper.find('.js-scroll-top').exists()).toBe(true);
expect(wrapper.find('.js-scroll-bottom').exists()).toBe(true);
});
describe('without append flag', () => {
it('replaces the trace', () =>
setupAndMount({
traceData: {
html: '<span>Different<span>',
status: 'running',
append: false,
complete: true,
},
}).then(() => {
expect(
wrapper
.find('.js-build-trace')
.text()
.trim(),
).toEqual('Different');
}));
});
describe('truncated information', () => {
describe('when size is less than total', () => {
it('shows information about truncated log', () => {
mock.onGet(`${props.pagePath}/trace.json`).reply(200, {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
complete: true,
});
return setupAndMount({
traceData: {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
complete: true,
},
}).then(() => {
expect(
wrapper
.find('.js-truncated-info')
.text()
.trim(),
).toContain('Showing last 50 bytes');
});
});
});
describe('when size is equal than total', () => {
it('does not show the truncated information', () =>
setupAndMount({
traceData: {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 100,
total: 100,
complete: true,
},
}).then(() => {
expect(
wrapper
.find('.js-truncated-info')
.text()
.trim(),
).toEqual('');
}));
});
it('should render link to raw ouput', () => {
expect(wrapper.find('.js-raw-link-controller').exists()).toBe(true);
});
describe('trace controls', () => {
beforeEach(() =>
setupAndMount({
traceData: {
html: '<span>Update</span>',
status: 'success',
append: false,
size: 50,
total: 100,
complete: true,
},
}),
);
it('should render scroll buttons', () => {
expect(wrapper.find('.js-scroll-top').exists()).toBe(true);
expect(wrapper.find('.js-scroll-bottom').exists()).toBe(true);
});
it('should render link to raw ouput', () => {
expect(wrapper.find('.js-raw-link-controller').exists()).toBe(true);
});
it('should render link to erase job', () => {
expect(wrapper.find('.js-erase-link').exists()).toBe(true);
});
it('should render link to erase job', () => {
expect(wrapper.find('.js-erase-link').exists()).toBe(true);
});
});
});
import Vue from 'vue';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import component from '~/jobs/components/job_log.vue';
import createStore from '~/jobs/store';
import { resetStore } from '../store/helpers';
describe('Job Log', () => {
const Component = Vue.extend(component);
let store;
let vm;
const trace =
'<span>Running with gitlab-runner 12.1.0 (de7731dd)<br/></span><span> on docker-auto-scale-com d5ae8d25<br/></span><div class="gl-mr-3" data-timestamp="1565502765" data-section="prepare-executor" role="button"></div><span class="section section-header js-s-prepare-executor">Using Docker executor with image ruby:2.6 ...<br/></span>';
beforeEach(() => {
store = createStore();
});
afterEach(() => {
resetStore(store);
vm.$destroy();
});
it('renders provided trace', () => {
vm = mountComponentWithStore(Component, {
props: {
trace,
isComplete: true,
},
store,
});
expect(vm.$el.querySelector('code').textContent).toContain(
'Running with gitlab-runner 12.1.0 (de7731dd)',
);
});
describe('while receiving trace', () => {
it('renders animation', () => {
vm = mountComponentWithStore(Component, {
props: {
trace,
isComplete: false,
},
store,
});
expect(vm.$el.querySelector('.js-log-animation')).not.toBeNull();
});
});
describe('when build trace has finishes', () => {
it('does not render animation', () => {
vm = mountComponentWithStore(Component, {
props: {
trace,
isComplete: true,
},
store,
});
expect(vm.$el.querySelector('.js-log-animation')).toBeNull();
});
});
});
......@@ -76,28 +76,15 @@ describe('Jobs Store Mutations', () => {
lines: [],
});
expect(stateCopy.trace).toEqual(html);
expect(stateCopy.traceSize).toEqual(511846);
expect(stateCopy.isTraceComplete).toEqual(true);
});
describe('with new job log', () => {
let stateWithNewLog;
beforeEach(() => {
gon.features = gon.features || {};
gon.features.jobLogJson = true;
stateWithNewLog = state();
});
afterEach(() => {
gon.features.jobLogJson = false;
});
describe('log.lines', () => {
describe('when append is true', () => {
it('sets the parsed log ', () => {
mutations[types.RECEIVE_TRACE_SUCCESS](stateWithNewLog, {
mutations[types.RECEIVE_TRACE_SUCCESS](stateCopy, {
append: true,
size: 511846,
complete: true,
......@@ -109,7 +96,7 @@ describe('Jobs Store Mutations', () => {
],
});
expect(stateWithNewLog.trace).toEqual([
expect(stateCopy.trace).toEqual([
{
offset: 1,
content: [{ text: 'Running with gitlab-runner 11.12.1 (5a147c92)' }],
......@@ -121,7 +108,7 @@ describe('Jobs Store Mutations', () => {
describe('when it is defined', () => {
it('sets the parsed log ', () => {
mutations[types.RECEIVE_TRACE_SUCCESS](stateWithNewLog, {
mutations[types.RECEIVE_TRACE_SUCCESS](stateCopy, {
append: false,
size: 511846,
complete: true,
......@@ -130,7 +117,7 @@ describe('Jobs Store Mutations', () => {
],
});
expect(stateWithNewLog.trace).toEqual([
expect(stateCopy.trace).toEqual([
{
offset: 0,
content: [{ text: 'Running with gitlab-runner 11.11.1 (5a147c92)' }],
......@@ -142,7 +129,7 @@ describe('Jobs Store Mutations', () => {
describe('when it is null', () => {
it('sets the default value', () => {
mutations[types.RECEIVE_TRACE_SUCCESS](stateWithNewLog, {
mutations[types.RECEIVE_TRACE_SUCCESS](stateCopy, {
append: true,
html,
size: 511846,
......@@ -150,7 +137,7 @@ describe('Jobs Store Mutations', () => {
lines: null,
});
expect(stateWithNewLog.trace).toEqual([]);
expect(stateCopy.trace).toEqual([]);
});
});
});
......
......@@ -11,7 +11,7 @@ RSpec.describe Ci::BuildTrace do
Gitlab::Ci::Trace::Stream.new { data }
end
subject { described_class.new(build: build, stream: stream, state: state, content_format: content_format) }
subject { described_class.new(build: build, stream: stream, state: state) }
shared_examples 'delegates methods' do
it { is_expected.to delegate_method(:state).to(:trace) }
......@@ -25,29 +25,11 @@ RSpec.describe Ci::BuildTrace do
it { is_expected.to delegate_method(:complete?).to(:build).with_prefix }
end
context 'with :json content format' do
let(:content_format) { :json }
it_behaves_like 'delegates methods'
it_behaves_like 'delegates methods'
it { is_expected.to be_json }
it 'returns formatted trace' do
expect(subject.trace.lines).to eq([
{ offset: 0, content: [{ text: 'the-stream' }] }
])
end
end
context 'with :html content format' do
let(:content_format) { :html }
it_behaves_like 'delegates methods'
it { is_expected.to be_html }
it 'returns formatted trace' do
expect(subject.trace.html).to eq('<span>the-stream</span>')
end
it 'returns formatted trace' do
expect(subject.lines).to eq([
{ offset: 0, content: [{ text: 'the-stream' }] }
])
end
end
......@@ -13,7 +13,7 @@ RSpec.describe BuildTraceEntity do
end
let(:build_trace) do
Ci::BuildTrace.new(build: build, stream: stream, content_format: content_format, state: nil)
Ci::BuildTrace.new(build: build, stream: stream, state: nil)
end
let(:entity) do
......@@ -22,42 +22,24 @@ RSpec.describe BuildTraceEntity do
subject { entity.as_json }
shared_examples 'includes build and trace metadata' do
it 'includes build attributes' do
expect(subject[:id]).to eq(build.id)
expect(subject[:status]).to eq(build.status)
expect(subject[:complete]).to eq(build.complete?)
end
it 'includes trace metadata' do
expect(subject).to include(:state)
expect(subject).to include(:append)
expect(subject).to include(:truncated)
expect(subject).to include(:offset)
expect(subject).to include(:size)
expect(subject).to include(:total)
end
it 'includes build attributes' do
expect(subject[:id]).to eq(build.id)
expect(subject[:status]).to eq(build.status)
expect(subject[:complete]).to eq(build.complete?)
end
context 'when content format is :json' do
let(:content_format) { :json }
it_behaves_like 'includes build and trace metadata'
it 'includes the trace content in json' do
expect(subject[:lines]).to eq([
{ offset: 0, content: [{ text: 'the-trace' }] }
])
end
it 'includes trace metadata' do
expect(subject).to include(:state)
expect(subject).to include(:append)
expect(subject).to include(:truncated)
expect(subject).to include(:offset)
expect(subject).to include(:size)
expect(subject).to include(:total)
end
context 'when content format is :html' do
let(:content_format) { :html }
it_behaves_like 'includes build and trace metadata'
it 'includes the trace content in json' do
expect(subject[:html]).to eq('<span>the-trace</span>')
end
it 'includes the trace content in json' do
expect(subject[:lines]).to eq([
{ offset: 0, content: [{ text: 'the-trace' }] }
])
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