Merge branch 'generate-spans-for-sections' into 'master'
Add collapsible sections to job log See merge request gitlab-org/gitlab-ce!28642
This commit is contained in:
commit
2634cad695
13 changed files with 323 additions and 57 deletions
|
@ -17,10 +17,19 @@ export default {
|
||||||
...mapState(['isScrolledToBottomBeforeReceivingTrace']),
|
...mapState(['isScrolledToBottomBeforeReceivingTrace']),
|
||||||
},
|
},
|
||||||
updated() {
|
updated() {
|
||||||
this.$nextTick(() => this.handleScrollDown());
|
this.$nextTick(() => {
|
||||||
|
this.handleScrollDown();
|
||||||
|
this.handleCollapsibleRows();
|
||||||
|
});
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.$nextTick(() => this.handleScrollDown());
|
this.$nextTick(() => {
|
||||||
|
this.handleScrollDown();
|
||||||
|
this.handleCollapsibleRows();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
destroyed() {
|
||||||
|
this.removeEventListener();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions(['scrollBottom']),
|
...mapActions(['scrollBottom']),
|
||||||
|
@ -38,21 +47,45 @@ export default {
|
||||||
}, 0);
|
}, 0);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
removeEventListener() {
|
||||||
|
this.$el
|
||||||
|
.querySelectorAll('.js-section-start')
|
||||||
|
.forEach(el => el.removeEventListener('click', this.handleSectionClick));
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* The collapsible rows are sent in HTML from the backend
|
||||||
|
* We need tos add a onclick handler for the divs that match `.js-section-start`
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
handleCollapsibleRows() {
|
||||||
|
this.$el
|
||||||
|
.querySelectorAll('.js-section-start')
|
||||||
|
.forEach(el => el.addEventListener('click', this.handleSectionClick));
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* On click, we toggle the hidden class of
|
||||||
|
* all the rows that match the `data-section` selector
|
||||||
|
*/
|
||||||
|
handleSectionClick(evt) {
|
||||||
|
const clickedArrow = evt.currentTarget;
|
||||||
|
// toggle the arrow class
|
||||||
|
clickedArrow.classList.toggle('fa-caret-right');
|
||||||
|
clickedArrow.classList.toggle('fa-caret-down');
|
||||||
|
|
||||||
|
const { section } = clickedArrow.dataset;
|
||||||
|
const sibilings = this.$el.querySelectorAll(`.js-s-${section}:not(.js-section-header)`);
|
||||||
|
|
||||||
|
sibilings.forEach(row => row.classList.toggle('hidden'));
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<pre class="js-build-trace build-trace qa-build-trace">
|
<pre class="js-build-trace build-trace qa-build-trace">
|
||||||
<code
|
<code class="bash" v-html="trace">
|
||||||
class="bash"
|
|
||||||
v-html="trace"
|
|
||||||
>
|
|
||||||
</code>
|
</code>
|
||||||
|
|
||||||
<div
|
<div v-if="!isComplete" class="js-log-animation build-loader-animation">
|
||||||
v-if="!isComplete"
|
|
||||||
class="js-log-animation build-loader-animation"
|
|
||||||
>
|
|
||||||
<div class="dot"></div>
|
<div class="dot"></div>
|
||||||
<div class="dot"></div>
|
<div class="dot"></div>
|
||||||
<div class="dot"></div>
|
<div class="dot"></div>
|
||||||
|
|
|
@ -124,6 +124,10 @@
|
||||||
float: left;
|
float: left;
|
||||||
padding-left: $gl-padding-8;
|
padding-left: $gl-padding-8;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.section-header ~ .section.line {
|
||||||
|
margin-left: $gl-padding;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.build-header {
|
.build-header {
|
||||||
|
|
5
changelogs/unreleased/generate-spans-for-sections.yml
Normal file
5
changelogs/unreleased/generate-spans-for-sections.yml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Adds collapsible sections for job log
|
||||||
|
merge_request: 28642
|
||||||
|
author:
|
||||||
|
type: added
|
|
@ -131,9 +131,9 @@ module Gitlab
|
||||||
|
|
||||||
def on_109(_) set_bg_color(9, 'l') end
|
def on_109(_) set_bg_color(9, 'l') end
|
||||||
|
|
||||||
attr_accessor :offset, :n_open_tags, :fg_color, :bg_color, :style_mask
|
attr_accessor :offset, :n_open_tags, :fg_color, :bg_color, :style_mask, :sections, :lineno_in_section
|
||||||
|
|
||||||
STATE_PARAMS = [:offset, :n_open_tags, :fg_color, :bg_color, :style_mask].freeze
|
STATE_PARAMS = [:offset, :n_open_tags, :fg_color, :bg_color, :style_mask, :sections, :lineno_in_section].freeze
|
||||||
|
|
||||||
def convert(stream, new_state)
|
def convert(stream, new_state)
|
||||||
reset_state
|
reset_state
|
||||||
|
@ -153,10 +153,9 @@ module Gitlab
|
||||||
|
|
||||||
start_offset = @offset
|
start_offset = @offset
|
||||||
|
|
||||||
open_new_tag
|
|
||||||
|
|
||||||
stream.each_line do |line|
|
stream.each_line do |line|
|
||||||
s = StringScanner.new(line)
|
s = StringScanner.new(line)
|
||||||
|
|
||||||
until s.eos?
|
until s.eos?
|
||||||
|
|
||||||
if s.scan(Gitlab::Regex.build_trace_section_regex)
|
if s.scan(Gitlab::Regex.build_trace_section_regex)
|
||||||
|
@ -166,11 +165,11 @@ module Gitlab
|
||||||
elsif s.scan(/\e(([@-_])(.*?)?)?$/)
|
elsif s.scan(/\e(([@-_])(.*?)?)?$/)
|
||||||
break
|
break
|
||||||
elsif s.scan(/</)
|
elsif s.scan(/</)
|
||||||
@out << '<'
|
write_in_tag '<'
|
||||||
elsif s.scan(/\r?\n/)
|
elsif s.scan(/\r?\n/)
|
||||||
@out << '<br>'
|
handle_new_line
|
||||||
else
|
else
|
||||||
@out << s.scan(/./m)
|
write_in_tag s.scan(/./m)
|
||||||
end
|
end
|
||||||
|
|
||||||
@offset += s.matched_size
|
@offset += s.matched_size
|
||||||
|
@ -190,13 +189,59 @@ module Gitlab
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def section_to_class_name(section)
|
||||||
|
section.to_s.downcase.gsub(/[^a-z0-9]/, '-')
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_new_line
|
||||||
|
css_classes = []
|
||||||
|
|
||||||
|
if @sections.any?
|
||||||
|
css_classes = %w[section line] + sections.map { |section| "s_#{section}" }
|
||||||
|
end
|
||||||
|
|
||||||
|
write_in_tag %{<br/>}
|
||||||
|
write_raw %{<span class="#{css_classes.join(' ')}"></span>} if css_classes.any?
|
||||||
|
@lineno_in_section += 1
|
||||||
|
open_new_tag
|
||||||
|
end
|
||||||
|
|
||||||
def handle_section(scanner)
|
def handle_section(scanner)
|
||||||
action = scanner[1]
|
action = scanner[1]
|
||||||
timestamp = scanner[2]
|
timestamp = scanner[2]
|
||||||
section = scanner[3]
|
section = scanner[3]
|
||||||
line = scanner.matched[0...-5] # strips \r\033[0K
|
|
||||||
|
|
||||||
@out << %{<div class="hidden" data-action="#{action}" data-timestamp="#{timestamp}" data-section="#{section}">#{line}</div>}
|
normalized_section = section_to_class_name(section)
|
||||||
|
|
||||||
|
if action == "start"
|
||||||
|
handle_section_start(normalized_section, timestamp)
|
||||||
|
elsif action == "end"
|
||||||
|
handle_section_end(normalized_section, timestamp)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_section_start(section, timestamp)
|
||||||
|
return if @sections.include?(section)
|
||||||
|
|
||||||
|
@sections << section
|
||||||
|
write_raw %{<div class="js-section-start fa fa-caret-down append-right-8 cursor-pointer" data-timestamp="#{timestamp}" data-section="#{data_section_names}" role="button"></div>}
|
||||||
|
@lineno_in_section = 0
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_section_end(section, timestamp)
|
||||||
|
return unless @sections.include?(section)
|
||||||
|
|
||||||
|
# close all sections up to section
|
||||||
|
until @sections.empty?
|
||||||
|
write_raw %{<div class="section-end" data-section="#{data_section_names}"></div>}
|
||||||
|
|
||||||
|
last_section = @sections.pop
|
||||||
|
break if section == last_section
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def data_section_names
|
||||||
|
@sections.join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_sequence(scanner)
|
def handle_sequence(scanner)
|
||||||
|
@ -217,8 +262,6 @@ module Gitlab
|
||||||
end
|
end
|
||||||
|
|
||||||
evaluate_command_stack(commands)
|
evaluate_command_stack(commands)
|
||||||
|
|
||||||
open_new_tag
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def evaluate_command_stack(stack)
|
def evaluate_command_stack(stack)
|
||||||
|
@ -231,6 +274,20 @@ module Gitlab
|
||||||
evaluate_command_stack(stack)
|
evaluate_command_stack(stack)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def write_in_tag(data)
|
||||||
|
ensure_open_new_tag
|
||||||
|
@out << data
|
||||||
|
end
|
||||||
|
|
||||||
|
def write_raw(data)
|
||||||
|
close_open_tags
|
||||||
|
@out << data
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_open_new_tag
|
||||||
|
open_new_tag if @n_open_tags == 0
|
||||||
|
end
|
||||||
|
|
||||||
def open_new_tag
|
def open_new_tag
|
||||||
css_classes = []
|
css_classes = []
|
||||||
|
|
||||||
|
@ -251,7 +308,11 @@ module Gitlab
|
||||||
css_classes << "term-#{css_class}" if @style_mask & flag != 0
|
css_classes << "term-#{css_class}" if @style_mask & flag != 0
|
||||||
end
|
end
|
||||||
|
|
||||||
return if css_classes.empty?
|
if @sections.any?
|
||||||
|
css_classes << "section"
|
||||||
|
css_classes << "js-section-header" if @lineno_in_section == 0
|
||||||
|
css_classes += sections.map { |section| "js-s-#{section}" }
|
||||||
|
end
|
||||||
|
|
||||||
@out << %{<span class="#{css_classes.join(' ')}">}
|
@out << %{<span class="#{css_classes.join(' ')}">}
|
||||||
@n_open_tags += 1
|
@n_open_tags += 1
|
||||||
|
@ -268,6 +329,8 @@ module Gitlab
|
||||||
@offset = 0
|
@offset = 0
|
||||||
@n_open_tags = 0
|
@n_open_tags = 0
|
||||||
@out = +''
|
@out = +''
|
||||||
|
@sections = []
|
||||||
|
@lineno_in_section = 0
|
||||||
reset
|
reset
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -540,7 +540,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do
|
||||||
expect(response).to have_gitlab_http_status(:ok)
|
expect(response).to have_gitlab_http_status(:ok)
|
||||||
expect(json_response['id']).to eq job.id
|
expect(json_response['id']).to eq job.id
|
||||||
expect(json_response['status']).to eq job.status
|
expect(json_response['status']).to eq job.status
|
||||||
expect(json_response['html']).to eq('BUILD TRACE')
|
expect(json_response['html']).to eq('<span class="">BUILD TRACE</span>')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -228,6 +228,26 @@ FactoryBot.define do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
trait :trace_with_duplicate_sections do
|
||||||
|
after(:create) do |build, evaluator|
|
||||||
|
trace = File.binread(
|
||||||
|
File.expand_path(
|
||||||
|
Rails.root.join('spec/fixtures/trace/trace_with_duplicate_sections')))
|
||||||
|
|
||||||
|
build.trace.set(trace)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
trait :trace_with_sections do
|
||||||
|
after(:create) do |build, evaluator|
|
||||||
|
trace = File.binread(
|
||||||
|
File.expand_path(
|
||||||
|
Rails.root.join('spec/fixtures/trace/trace_with_sections')))
|
||||||
|
|
||||||
|
build.trace.set(trace)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
trait :unicode_trace_live do
|
trait :unicode_trace_live do
|
||||||
after(:create) do |build, evaluator|
|
after(:create) do |build, evaluator|
|
||||||
trace = File.binread(
|
trace = File.binread(
|
||||||
|
|
|
@ -34,6 +34,52 @@ describe 'User browses a job', :js do
|
||||||
expect(page).to have_content('Job has been erased')
|
expect(page).to have_content('Job has been erased')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
shared_examples 'has collapsible sections' do
|
||||||
|
it 'collapses the section clicked' do
|
||||||
|
wait_for_requests
|
||||||
|
text_to_hide = "Cloning into '/nolith/ci-tests'"
|
||||||
|
text_to_show = 'Waiting for pod'
|
||||||
|
|
||||||
|
expect(page).to have_content(text_to_hide)
|
||||||
|
expect(page).to have_content(text_to_show)
|
||||||
|
|
||||||
|
first('.js-section-start[data-section="get-sources"]').click
|
||||||
|
|
||||||
|
expect(page).not_to have_content(text_to_hide)
|
||||||
|
expect(page).to have_content(text_to_show)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when job trace contains sections' do
|
||||||
|
let!(:build) { create(:ci_build, :success, :trace_with_sections, :coverage, pipeline: pipeline) }
|
||||||
|
|
||||||
|
it_behaves_like 'has collapsible sections'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when job trace contains duplicate sections' do
|
||||||
|
let!(:build) { create(:ci_build, :success, :trace_with_duplicate_sections, :coverage, pipeline: pipeline) }
|
||||||
|
|
||||||
|
it_behaves_like 'has collapsible sections'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when job trace contains sections' do
|
||||||
|
let!(:build) { create(:ci_build, :success, :trace_with_duplicate_sections, :coverage, pipeline: pipeline) }
|
||||||
|
|
||||||
|
it 'collapses a section' do
|
||||||
|
wait_for_requests
|
||||||
|
text_to_hide = "Cloning into '/nolith/ci-tests'"
|
||||||
|
text_to_show = 'Waiting for pod'
|
||||||
|
|
||||||
|
expect(page).to have_content(text_to_hide)
|
||||||
|
expect(page).to have_content(text_to_show)
|
||||||
|
|
||||||
|
first('.js-section-start[data-section="get-sources"]').click
|
||||||
|
|
||||||
|
expect(page).not_to have_content(text_to_hide)
|
||||||
|
expect(page).to have_content(text_to_show)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'with a failed job' do
|
context 'with a failed job' do
|
||||||
let!(:build) { create(:ci_build, :failed, :trace_artifact, pipeline: pipeline) }
|
let!(:build) { create(:ci_build, :failed, :trace_artifact, pipeline: pipeline) }
|
||||||
|
|
||||||
|
|
30
spec/fixtures/trace/trace_with_duplicate_sections
vendored
Normal file
30
spec/fixtures/trace/trace_with_duplicate_sections
vendored
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
[0KRunning with gitlab-runner dev (HEAD)
|
||||||
|
on kitsune minikube (a21b584f)
|
||||||
|
[0;m[0;33mWARNING: Namespace is empty, therefore assuming 'default'.
|
||||||
|
[0;m[0KUsing Kubernetes namespace: default
|
||||||
|
[0;m[0KUsing Kubernetes executor with image alpine:3.4 ...
|
||||||
|
[0;msection_start:1506004954:prepare_script
[0KWaiting for pod default/runner-a21b584f-project-1208199-concurrent-0sg03f to be running, status is Pending
|
||||||
|
Running on runner-a21b584f-project-1208199-concurrent-0sg03f via kitsune.local...
|
||||||
|
section_end:1506004957:prepare_script
[0Ksection_start:1506004957:get_sources
[0K[32;1mCloning repository...[0;m
|
||||||
|
Cloning into '/nolith/ci-tests'...
|
||||||
|
[32;1mChecking out dddd7a6e as master...[0;m
|
||||||
|
[32;1mSkipping Git submodules setup[0;m
|
||||||
|
section_end:1506004958:get_sources
[0Ksection_start:1506004958:restore_cache
[0Ksection_end:1506004958:restore_cache
[0Ksection_start:1506004958:download_artifacts
[0Ksection_end:1506004958:download_artifacts
[0Ksection_start:1506004958:build_script
[0K[32;1m$ whoami[0;m
|
||||||
|
root
|
||||||
|
section_end:1506004959:build_script
[0Ksection_start:1506004959:after_script
[0Ksection_end:1506004959:after_script
[0Ksection_start:1506004959:archive_cache
[0Ksection_end:1506004959:archive_cache
[0Ksection_start:1506004959:upload_artifacts
[0Ksection_end:1506004959:upload_artifacts
[0K[32;1mJob succeeded
|
||||||
|
[0;m
|
||||||
|
[0KRunning with gitlab-runner dev (HEAD)
|
||||||
|
on kitsune minikube (a21b584f)
|
||||||
|
[0;m[0;33mWARNING: Namespace is empty, therefore assuming 'default'.
|
||||||
|
[0;m[0KUsing Kubernetes namespace: default
|
||||||
|
[0;m[0KUsing Kubernetes executor with image alpine:3.4 ...
|
||||||
|
[0;msection_start:1506004954:prepare_script
[0KWaiting for pod default/runner-a21b584f-project-1208199-concurrent-0sg03f to be running, status is Pending
|
||||||
|
Running on runner-a21b584f-project-1208199-concurrent-0sg03f via kitsune.local...
|
||||||
|
section_end:1506004957:prepare_script
[0Ksection_start:1506004957:get_sources
[0K[32;1mCloning repository...[0;m
|
||||||
|
Cloning into '/nolith/ci-tests'...
|
||||||
|
[32;1mChecking out dddd7a6e as master...[0;m
|
||||||
|
[32;1mSkipping Git submodules setup[0;m
|
||||||
|
section_end:1506004958:get_sources
[0Ksection_start:1506004958:restore_cache
[0Ksection_end:1506004958:restore_cache
[0Ksection_start:1506004958:download_artifacts
[0Ksection_end:1506004958:download_artifacts
[0Ksection_start:1506004958:build_script
[0K[32;1m$ whoami[0;m
|
||||||
|
root
|
||||||
|
section_end:1506004959:build_script
[0Ksection_start:1506004959:after_script
[0Ksection_end:1506004959:after_script
[0Ksection_start:1506004959:archive_cache
[0Ksection_end:1506004959:archive_cache
[0Ksection_start:1506004959:upload_artifacts
[0Ksection_end:1506004959:upload_artifacts
[0K[32;1mJob succeeded
|
||||||
|
[0;m
|
|
@ -3,6 +3,7 @@ import component from '~/jobs/components/job_log.vue';
|
||||||
import createStore from '~/jobs/store';
|
import createStore from '~/jobs/store';
|
||||||
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
|
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
|
||||||
import { resetStore } from '../store/helpers';
|
import { resetStore } from '../store/helpers';
|
||||||
|
import { logWithCollapsibleSections } from '../mock_data';
|
||||||
|
|
||||||
describe('Job Log', () => {
|
describe('Job Log', () => {
|
||||||
const Component = Vue.extend(component);
|
const Component = Vue.extend(component);
|
||||||
|
@ -62,4 +63,40 @@ describe('Job Log', () => {
|
||||||
expect(vm.$el.querySelector('.js-log-animation')).toBeNull();
|
expect(vm.$el.querySelector('.js-log-animation')).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Collapsible sections', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vm = mountComponentWithStore(Component, {
|
||||||
|
props: {
|
||||||
|
trace: logWithCollapsibleSections.html,
|
||||||
|
isComplete: true,
|
||||||
|
},
|
||||||
|
store,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders open arrow', () => {
|
||||||
|
expect(vm.$el.querySelector('.fa-caret-down')).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('toggles hidden class to the sibilings rows when arrow is clicked', done => {
|
||||||
|
vm.$nextTick()
|
||||||
|
.then(() => {
|
||||||
|
const { section } = vm.$el.querySelector('.js-section-start').dataset;
|
||||||
|
vm.$el.querySelector('.js-section-start').click();
|
||||||
|
|
||||||
|
vm.$el.querySelectorAll(`.js-s-${section}:not(.js-section-header)`).forEach(el => {
|
||||||
|
expect(el.classList.contains('hidden')).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
vm.$el.querySelector('.js-section-start').click();
|
||||||
|
|
||||||
|
vm.$el.querySelectorAll(`.js-s-${section}:not(.js-section-header)`).forEach(el => {
|
||||||
|
expect(el.classList.contains('hidden')).toEqual(false);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(done)
|
||||||
|
.catch(done.fail);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1189,3 +1189,18 @@ export const jobsInStage = {
|
||||||
path: '/gitlab-org/gitlab-shell/pipelines/27#build',
|
path: '/gitlab-org/gitlab-shell/pipelines/27#build',
|
||||||
dropdown_path: '/gitlab-org/gitlab-shell/pipelines/27/stage.json?stage=build',
|
dropdown_path: '/gitlab-org/gitlab-shell/pipelines/27/stage.json?stage=build',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const logWithCollapsibleSections = {
|
||||||
|
append: false,
|
||||||
|
complete: true,
|
||||||
|
html:
|
||||||
|
'<div class="js-section-start fa fa-caret-down append-right-8 cursor-pointer" data-timestamp="1559571405" data-section="after-script" role="button"></div><span class="term-fg-l-green term-bold section js-section-header js-s-after-script">Running after script...</span><span class="section js-section-header js-s-after-script"><br /></span><span class="section s_after-script line"></span><span class="section js-s-after-script"></span><span class="term-fg-l-green term-bold section js-s-after-script">$ date</span><span class="section js-s-after-script"><br /></span><span class="section s_after-script line"></span><span class="section js-s-after-script">Mon Jun 3 14:16:46 UTC 2019<br /></span><span class="section s_after-script line"></span><span class="section js-s-after-script"></span><div class="section-end" data-section="after-script"></div><div class="js-section-start fa fa-caret-down append-right-8 cursor-pointer"data-timestamp="1559571408" data-section="archive-cache" role="button" ></div><span class="term-fg-l-green term-bold section js-section-header js-s-archive-cache">Not uploading cache debian-stretch-ruby-2.6.3-node-10.x-3 due to policy</span><span class="section js-section-header js-s-archive-cache"><br /></span><span class="section s_archive-cache line"></span><span class="section js-s-archive-cache"></span><div class="section-end" data-section="archive-cache"></div><div class="js-section-start fa fa-caret-down append-right-8 cursor-pointer" data-timestamp="1559571409" data-section="upload-artifacts-on-success" role="button"></div><span class="term-fg-l-green term-bold section js-section-header js-s-upload-artifacts-on-success">Uploading artifacts...</span><span class="section js-section-header js-s-upload-artifacts-on-success"><br /></span><span class="section s_upload-artifacts-on-success line"></span><span class="section js-s-upload-artifacts-on-success">coverage/: found 5 matching files </span><span class="section js-s-upload-artifacts-on-success"> <br /></span><span class="section s_upload-artifacts-on-success line"></span><span class="section js-s-upload-artifacts-on-success">knapsack/: found 4 matching files </span><span class="section js-s-upload-artifacts-on-success"> <br /></span><span class="section s_upload-artifacts-on-success line"></span><span class="section js-s-upload-artifacts-on-success">rspec_flaky/: found 4 matching files </span><span class="section js-s-upload-artifacts-on-success"> <br /></span><span class="section s_upload-artifacts-on-success line"></span><span class="section js-s-upload-artifacts-on-success">rspec_profiling/: found 1 matching files </span><span class="section js-s-upload-artifacts-on-success"> <br /></span><span class="section s_upload-artifacts-on-success line"></span><span class="section js-s-upload-artifacts-on-success"></span><span class="term-fg-yellow section js-s-upload-artifacts-on-success">WARNING: tmp/capybara/: no matching files </span><span class="section js-s-upload-artifacts-on-success"> <br /></span><span class="section s_upload-artifacts-on-success line"></span><span class="section js-s-upload-artifacts-on-success">Uploading artifacts to coordinator... ok </span><span class="section js-s-upload-artifacts-on-success"> id</span><span class="section js-s-upload-artifacts-on-success">=224162288 responseStatus</span><span class="section js-s-upload-artifacts-on-success">=201 Created token</span><span class="section js-s-upload-artifacts-on-success">=bBmyXJNW<br /></span><span class="section s_upload-artifacts-on-success line"></span><span class="section js-s-upload-artifacts-on-success"></span><span class="term-fg-l-green term-bold section js-s-upload-artifacts-on-success">Uploading artifacts...</span><span class="section js-s-upload-artifacts-on-success"><br /></span><span class="section s_upload-artifacts-on-success line"></span><span class="section js-s-upload-artifacts-on-success">junit_rspec.xml: found 1 matching files </span><span class="section js-s-upload-artifacts-on-success"> <br /></span><span class="section s_upload-artifacts-on-success line"></span><span class="section js-s-upload-artifacts-on-success">Uploading artifacts to coordinator... ok </span><span class="section js-s-upload-artifacts-on-success"> id</span><span class="section js-s-upload-artifacts-on-success">=224162288 responseStatus</span><span class="section js-s-upload-artifacts-on-success">=201 Created token</span><span class="section js-s-upload-artifacts-on-success">=bBmyXJNW<br /></span><span class="section s_upload-artifacts-on-success line"></span><span class="section js-s-upload-artifacts-on-success"></span><div class="section-end" data-section="upload-artifacts-on-success"></div><span class="term-fg-l-green term-bold">Job succeeded<br /><span class="term-fg-l-green term-bold"></span></span>',
|
||||||
|
id: 1385,
|
||||||
|
offset: 0,
|
||||||
|
size: 78815,
|
||||||
|
state:
|
||||||
|
'eyJvZmZzZXQiOjc4ODE1LCJuX29wZW5fdGFncyI6MCwiZmdfY29sb3IiOm51bGwsImJnX2NvbG9yIjpudWxsLCJzdHlsZV9tYXNrIjowLCJzZWN0aW9ucyI6W10sImxpbmVub19pbl9zZWN0aW9uIjoxMX0=',
|
||||||
|
status: 'success',
|
||||||
|
total: 78815,
|
||||||
|
truncated: false,
|
||||||
|
};
|
||||||
|
|
|
@ -4,11 +4,11 @@ describe Gitlab::Ci::Ansi2html do
|
||||||
subject { described_class }
|
subject { described_class }
|
||||||
|
|
||||||
it "prints non-ansi as-is" do
|
it "prints non-ansi as-is" do
|
||||||
expect(convert_html("Hello")).to eq('Hello')
|
expect(convert_html("Hello")).to eq('<span class="">Hello</span>')
|
||||||
end
|
end
|
||||||
|
|
||||||
it "strips non-color-changing control sequences" do
|
it "strips non-color-changing control sequences" do
|
||||||
expect(convert_html("Hello \e[2Kworld")).to eq('Hello world')
|
expect(convert_html("Hello \e[2Kworld")).to eq('<span class="">Hello world</span>')
|
||||||
end
|
end
|
||||||
|
|
||||||
it "prints simply red" do
|
it "prints simply red" do
|
||||||
|
@ -32,7 +32,7 @@ describe Gitlab::Ci::Ansi2html do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "resets colors after red on blue" do
|
it "resets colors after red on blue" do
|
||||||
expect(convert_html("\e[31;44mHello\e[0m world")).to eq('<span class="term-fg-red term-bg-blue">Hello</span> world')
|
expect(convert_html("\e[31;44mHello\e[0m world")).to eq('<span class="term-fg-red term-bg-blue">Hello</span><span class=""> world</span>')
|
||||||
end
|
end
|
||||||
|
|
||||||
it "performs color change from red/blue to yellow/blue" do
|
it "performs color change from red/blue to yellow/blue" do
|
||||||
|
@ -44,11 +44,11 @@ describe Gitlab::Ci::Ansi2html do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "performs color change from red/blue to reset to yellow/green" do
|
it "performs color change from red/blue to reset to yellow/green" do
|
||||||
expect(convert_html("\e[31;44mHello\e[0m \e[33;42mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello</span> <span class="term-fg-yellow term-bg-green">world</span>')
|
expect(convert_html("\e[31;44mHello\e[0m \e[33;42mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello</span><span class=""> </span><span class="term-fg-yellow term-bg-green">world</span>')
|
||||||
end
|
end
|
||||||
|
|
||||||
it "ignores unsupported codes" do
|
it "ignores unsupported codes" do
|
||||||
expect(convert_html("\e[51mHello\e[0m")).to eq('Hello')
|
expect(convert_html("\e[51mHello\e[0m")).to eq('<span class="">Hello</span>')
|
||||||
end
|
end
|
||||||
|
|
||||||
it "prints light red" do
|
it "prints light red" do
|
||||||
|
@ -72,8 +72,8 @@ describe Gitlab::Ci::Ansi2html do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "resets bold text" do
|
it "resets bold text" do
|
||||||
expect(convert_html("\e[1mHello\e[21m world")).to eq('<span class="term-bold">Hello</span> world')
|
expect(convert_html("\e[1mHello\e[21m world")).to eq('<span class="term-bold">Hello</span><span class=""> world</span>')
|
||||||
expect(convert_html("\e[1mHello\e[22m world")).to eq('<span class="term-bold">Hello</span> world')
|
expect(convert_html("\e[1mHello\e[22m world")).to eq('<span class="term-bold">Hello</span><span class=""> world</span>')
|
||||||
end
|
end
|
||||||
|
|
||||||
it "prints italic text" do
|
it "prints italic text" do
|
||||||
|
@ -81,7 +81,7 @@ describe Gitlab::Ci::Ansi2html do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "resets italic text" do
|
it "resets italic text" do
|
||||||
expect(convert_html("\e[3mHello\e[23m world")).to eq('<span class="term-italic">Hello</span> world')
|
expect(convert_html("\e[3mHello\e[23m world")).to eq('<span class="term-italic">Hello</span><span class=""> world</span>')
|
||||||
end
|
end
|
||||||
|
|
||||||
it "prints underlined text" do
|
it "prints underlined text" do
|
||||||
|
@ -89,7 +89,7 @@ describe Gitlab::Ci::Ansi2html do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "resets underlined text" do
|
it "resets underlined text" do
|
||||||
expect(convert_html("\e[4mHello\e[24m world")).to eq('<span class="term-underline">Hello</span> world')
|
expect(convert_html("\e[4mHello\e[24m world")).to eq('<span class="term-underline">Hello</span><span class=""> world</span>')
|
||||||
end
|
end
|
||||||
|
|
||||||
it "prints concealed text" do
|
it "prints concealed text" do
|
||||||
|
@ -97,7 +97,7 @@ describe Gitlab::Ci::Ansi2html do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "resets concealed text" do
|
it "resets concealed text" do
|
||||||
expect(convert_html("\e[8mHello\e[28m world")).to eq('<span class="term-conceal">Hello</span> world')
|
expect(convert_html("\e[8mHello\e[28m world")).to eq('<span class="term-conceal">Hello</span><span class=""> world</span>')
|
||||||
end
|
end
|
||||||
|
|
||||||
it "prints crossed-out text" do
|
it "prints crossed-out text" do
|
||||||
|
@ -105,7 +105,7 @@ describe Gitlab::Ci::Ansi2html do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "resets crossed-out text" do
|
it "resets crossed-out text" do
|
||||||
expect(convert_html("\e[9mHello\e[29m world")).to eq('<span class="term-cross">Hello</span> world')
|
expect(convert_html("\e[9mHello\e[29m world")).to eq('<span class="term-cross">Hello</span><span class=""> world</span>')
|
||||||
end
|
end
|
||||||
|
|
||||||
it "can print 256 xterm fg colors" do
|
it "can print 256 xterm fg colors" do
|
||||||
|
@ -137,15 +137,15 @@ describe Gitlab::Ci::Ansi2html do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "prints <" do
|
it "prints <" do
|
||||||
expect(convert_html("<")).to eq('<')
|
expect(convert_html("<")).to eq('<span class=""><</span>')
|
||||||
end
|
end
|
||||||
|
|
||||||
it "replaces newlines with line break tags" do
|
it "replaces newlines with line break tags" do
|
||||||
expect(convert_html("\n")).to eq('<br>')
|
expect(convert_html("\n")).to eq('<span class=""><br/><span class=""></span></span>')
|
||||||
end
|
end
|
||||||
|
|
||||||
it "groups carriage returns with newlines" do
|
it "groups carriage returns with newlines" do
|
||||||
expect(convert_html("\r\n")).to eq('<br>')
|
expect(convert_html("\r\n")).to eq('<span class=""><br/><span class=""></span></span>')
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "incremental update" do
|
describe "incremental update" do
|
||||||
|
@ -166,14 +166,14 @@ describe Gitlab::Ci::Ansi2html do
|
||||||
let(:pre_text) { "\e[1mHello" }
|
let(:pre_text) { "\e[1mHello" }
|
||||||
let(:pre_html) { "<span class=\"term-bold\">Hello</span>" }
|
let(:pre_html) { "<span class=\"term-bold\">Hello</span>" }
|
||||||
let(:text) { "\e[1mWorld" }
|
let(:text) { "\e[1mWorld" }
|
||||||
let(:html) { "<span class=\"term-bold\"></span><span class=\"term-bold\">World</span>" }
|
let(:html) { "<span class=\"term-bold\">World</span>" }
|
||||||
|
|
||||||
it_behaves_like 'stateable converter'
|
it_behaves_like 'stateable converter'
|
||||||
end
|
end
|
||||||
|
|
||||||
context "with split sequence" do
|
context "with split sequence" do
|
||||||
let(:pre_text) { "\e[1m" }
|
let(:pre_text) { "\e[1m" }
|
||||||
let(:pre_html) { "<span class=\"term-bold\"></span>" }
|
let(:pre_html) { "" }
|
||||||
let(:text) { "Hello" }
|
let(:text) { "Hello" }
|
||||||
let(:html) { "<span class=\"term-bold\">Hello</span>" }
|
let(:html) { "<span class=\"term-bold\">Hello</span>" }
|
||||||
|
|
||||||
|
@ -182,7 +182,7 @@ describe Gitlab::Ci::Ansi2html do
|
||||||
|
|
||||||
context "with partial sequence" do
|
context "with partial sequence" do
|
||||||
let(:pre_text) { "Hello\e" }
|
let(:pre_text) { "Hello\e" }
|
||||||
let(:pre_html) { "Hello" }
|
let(:pre_html) { "<span class=\"\">Hello</span>" }
|
||||||
let(:text) { "[1m World" }
|
let(:text) { "[1m World" }
|
||||||
let(:html) { "<span class=\"term-bold\"> World</span>" }
|
let(:html) { "<span class=\"term-bold\"> World</span>" }
|
||||||
|
|
||||||
|
@ -191,9 +191,9 @@ describe Gitlab::Ci::Ansi2html do
|
||||||
|
|
||||||
context 'with new line' do
|
context 'with new line' do
|
||||||
let(:pre_text) { "Hello\r" }
|
let(:pre_text) { "Hello\r" }
|
||||||
let(:pre_html) { "Hello\r" }
|
let(:pre_html) { "<span class=\"\">Hello\r</span>" }
|
||||||
let(:text) { "\nWorld" }
|
let(:text) { "\nWorld" }
|
||||||
let(:html) { "<br>World" }
|
let(:html) { "<span class=\"\"><br/><span class=\"\">World</span></span>" }
|
||||||
|
|
||||||
it_behaves_like 'stateable converter'
|
it_behaves_like 'stateable converter'
|
||||||
end
|
end
|
||||||
|
@ -207,20 +207,20 @@ describe Gitlab::Ci::Ansi2html do
|
||||||
let(:section_start) { "section_start:#{section_start_time.to_i}:#{section_name}\r\033[0K"}
|
let(:section_start) { "section_start:#{section_start_time.to_i}:#{section_name}\r\033[0K"}
|
||||||
let(:section_end) { "section_end:#{section_end_time.to_i}:#{section_name}\r\033[0K"}
|
let(:section_end) { "section_end:#{section_end_time.to_i}:#{section_name}\r\033[0K"}
|
||||||
let(:section_start_html) do
|
let(:section_start_html) do
|
||||||
'<div class="hidden" data-action="start"'\
|
'<div class="js-section-start fa fa-caret-down append-right-8 cursor-pointer"' \
|
||||||
" data-timestamp=\"#{section_start_time.to_i}\" data-section=\"#{section_name}\">"\
|
" data-timestamp=\"#{section_start_time.to_i}\" data-section=\"#{class_name(section_name)}\"" \
|
||||||
"#{section_start[0...-5]}</div>"
|
' role="button"></div>'
|
||||||
end
|
end
|
||||||
let(:section_end_html) do
|
let(:section_end_html) do
|
||||||
'<div class="hidden" data-action="end"'\
|
"<div class=\"section-end\" data-section=\"#{class_name(section_name)}\"></div>"
|
||||||
" data-timestamp=\"#{section_end_time.to_i}\" data-section=\"#{section_name}\">"\
|
|
||||||
"#{section_end[0...-5]}</div>"
|
|
||||||
end
|
end
|
||||||
|
|
||||||
shared_examples 'forbidden char in section_name' do
|
shared_examples 'forbidden char in section_name' do
|
||||||
it 'ignores sections' do
|
it 'ignores sections' do
|
||||||
text = "#{section_start}Some text#{section_end}"
|
text = "#{section_start}Some text#{section_end}"
|
||||||
html = text.gsub("\033[0K", '').gsub('<', '<')
|
class_name_start = section_start.gsub("\033[0K", '').gsub('<', '<')
|
||||||
|
class_name_end = section_end.gsub("\033[0K", '').gsub('<', '<')
|
||||||
|
html = %{<span class="">#{class_name_start}Some text#{class_name_end}</span>}
|
||||||
|
|
||||||
expect(convert_html(text)).to eq(html)
|
expect(convert_html(text)).to eq(html)
|
||||||
end
|
end
|
||||||
|
@ -231,7 +231,11 @@ describe Gitlab::Ci::Ansi2html do
|
||||||
|
|
||||||
it 'prints light red' do
|
it 'prints light red' do
|
||||||
text = "#{section_start}\e[91mHello\e[0m\n#{section_end}"
|
text = "#{section_start}\e[91mHello\e[0m\n#{section_end}"
|
||||||
html = %{#{section_start_html}<span class="term-fg-l-red">Hello</span><br>#{section_end_html}}
|
header = %{<span class="term-fg-l-red section js-section-header js-s-#{class_name(section_name)}">Hello</span>}
|
||||||
|
line_break = %{<span class="section js-section-header js-s-#{class_name(section_name)}"><br/></span>}
|
||||||
|
line = %{<span class="section line s_#{class_name(section_name)}"></span>}
|
||||||
|
empty_line = %{<span class="section js-s-#{class_name(section_name)}"></span>}
|
||||||
|
html = "#{section_start_html}#{header}#{line_break}#{line}#{empty_line}#{section_end_html}"
|
||||||
|
|
||||||
expect(convert_html(text)).to eq(html)
|
expect(convert_html(text)).to eq(html)
|
||||||
end
|
end
|
||||||
|
@ -294,4 +298,8 @@ describe Gitlab::Ci::Ansi2html do
|
||||||
stream = StringIO.new(data)
|
stream = StringIO.new(data)
|
||||||
subject.convert(stream).html
|
subject.convert(stream).html
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def class_name(section)
|
||||||
|
subject::Converter.new.section_to_class_name(section)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -64,7 +64,10 @@ describe Gitlab::Ci::Trace::Stream, :clean_gitlab_redis_cache do
|
||||||
|
|
||||||
result = stream.html
|
result = stream.html
|
||||||
|
|
||||||
expect(result).to eq("ヾ(´༎ຶД༎ຶ`)ノ<br><span class=\"term-fg-green\">許功蓋</span><br>")
|
expect(result).to eq(
|
||||||
|
"<span class=\"\">ヾ(´༎ຶД༎ຶ`)ノ<br/><span class=\"\"></span></span>"\
|
||||||
|
"<span class=\"term-fg-green\">許功蓋</span><span class=\"\"><br/>"\
|
||||||
|
"<span class=\"\"></span></span>")
|
||||||
expect(result.encoding).to eq(Encoding.default_external)
|
expect(result.encoding).to eq(Encoding.default_external)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -250,7 +253,7 @@ describe Gitlab::Ci::Trace::Stream, :clean_gitlab_redis_cache do
|
||||||
it 'returns html content with state' do
|
it 'returns html content with state' do
|
||||||
result = stream.html_with_state
|
result = stream.html_with_state
|
||||||
|
|
||||||
expect(result.html).to eq("1234")
|
expect(result.html).to eq("<span class=\"\">1234</span>")
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'follow-up state' do
|
context 'follow-up state' do
|
||||||
|
@ -266,7 +269,7 @@ describe Gitlab::Ci::Trace::Stream, :clean_gitlab_redis_cache do
|
||||||
result = stream.html_with_state(last_result.state)
|
result = stream.html_with_state(last_result.state)
|
||||||
|
|
||||||
expect(result.append).to be_truthy
|
expect(result.append).to be_truthy
|
||||||
expect(result.html).to eq("5678")
|
expect(result.html).to eq("<span class=\"\">5678</span>")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -302,11 +305,13 @@ describe Gitlab::Ci::Trace::Stream, :clean_gitlab_redis_cache do
|
||||||
describe '#html' do
|
describe '#html' do
|
||||||
shared_examples_for 'htmls' do
|
shared_examples_for 'htmls' do
|
||||||
it "returns html" do
|
it "returns html" do
|
||||||
expect(stream.html).to eq("12<br>34<br>56")
|
expect(stream.html).to eq(
|
||||||
|
"<span class=\"\">12<br/><span class=\"\">34<br/>"\
|
||||||
|
"<span class=\"\">56</span></span></span>")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns html for last line only" do
|
it "returns html for last line only" do
|
||||||
expect(stream.html(last_lines: 1)).to eq("56")
|
expect(stream.html(last_lines: 1)).to eq("<span class=\"\">56</span>")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -5,11 +5,11 @@ shared_examples_for 'common trace features' do
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns formatted html" do
|
it "returns formatted html" do
|
||||||
expect(trace.html).to eq("12<br>34")
|
expect(trace.html).to eq("<span class=\"\">12<br/><span class=\"\">34</span></span>")
|
||||||
end
|
end
|
||||||
|
|
||||||
it "returns last line of formatted html" do
|
it "returns last line of formatted html" do
|
||||||
expect(trace.html(last_lines: 1)).to eq("34")
|
expect(trace.html(last_lines: 1)).to eq("<span class=\"\">34</span>")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue