stream_spec.rb 12 KB
Newer Older
1 2
require 'spec_helper'

3
describe Gitlab::Ci::Trace::Stream, :clean_gitlab_redis_cache do
4
  set(:build) { create(:ci_build, :running) }
5 6 7 8 9

  before do
    stub_feature_flags(ci_enable_live_trace: true)
  end

10
  describe 'delegates' do
Lin Jen-Shin's avatar
Lin Jen-Shin committed
11
    subject { described_class.new { nil } }
12 13 14 15 16 17 18 19 20 21 22

    it { is_expected.to delegate_method(:close).to(:stream) }
    it { is_expected.to delegate_method(:tell).to(:stream) }
    it { is_expected.to delegate_method(:seek).to(:stream) }
    it { is_expected.to delegate_method(:size).to(:stream) }
    it { is_expected.to delegate_method(:path).to(:stream) }
    it { is_expected.to delegate_method(:truncate).to(:stream) }
    it { is_expected.to delegate_method(:valid?).to(:stream).as(:present?) }
  end

  describe '#limit' do
23 24 25 26 27
    shared_examples_for 'limits' do
      it 'if size is larger we start from beginning' do
        stream.limit(20)

        expect(stream.tell).to eq(0)
28 29
      end

30 31
      it 'if size is smaller we start from the end' do
        stream.limit(2)
32

33 34
        expect(stream.raw).to eq("8")
      end
35

36 37 38 39 40 41
      context 'when the trace contains ANSI sequence and Unicode' do
        let(:stream) do
          described_class.new do
            File.open(expand_fixture_path('trace/ansi-sequence-and-unicode'))
          end
        end
42

43 44
        it 'forwards to the next linefeed, case 1' do
          stream.limit(7)
45

46 47 48 49
          result = stream.raw

          expect(result).to eq('')
          expect(result.encoding).to eq(Encoding.default_external)
50
        end
51

52 53
        it 'forwards to the next linefeed, case 2' do
          stream.limit(29)
54

55
          result = stream.raw
56

57 58 59
          expect(result).to eq("\e[01;32m許功蓋\e[0m\n")
          expect(result.encoding).to eq(Encoding.default_external)
        end
60

61 62 63
        # See https://gitlab.com/gitlab-org/gitlab-ce/issues/30796
        it 'reads in binary, output as Encoding.default_external' do
          stream.limit(52)
64

65
          result = stream.html
66

67 68 69
          expect(result).to eq("ヾ(´༎ຶД༎ຶ`)ノ<br><span class=\"term-fg-green\">許功蓋</span><br>")
          expect(result.encoding).to eq(Encoding.default_external)
        end
70
      end
71
    end
72

73 74 75 76 77 78
    context 'when stream is StringIO' do
      let(:stream) do
        described_class.new do
          StringIO.new((1..8).to_a.join("\n"))
        end
      end
79

80 81
      it_behaves_like 'limits'
    end
82

83 84 85
    context 'when stream is ChunkedIO' do
      let(:stream) do
        described_class.new do
86
          Gitlab::Ci::Trace::ChunkedIO.new(build).tap do |chunked_io|
87 88 89 90
            chunked_io.write((1..8).to_a.join("\n"))
            chunked_io.seek(0, IO::SEEK_SET)
          end
        end
91
      end
92 93

      it_behaves_like 'limits'
94
    end
95 96 97
  end

  describe '#append' do
98 99 100 101
    shared_examples_for 'appends' do
      it "truncates and append content" do
        stream.append("89", 4)
        stream.seek(0)
102

103 104
        expect(stream.size).to eq(6)
        expect(stream.raw).to eq("123489")
105 106
      end

107 108 109 110
      it 'appends in binary mode' do
        '😺'.force_encoding('ASCII-8BIT').each_char.with_index do |byte, offset|
          stream.append(byte, offset)
        end
111

112
        stream.seek(0)
113

114 115 116
        expect(stream.size).to eq(4)
        expect(stream.raw).to eq('😺')
      end
117
    end
118

119
    context 'when stream is Tempfile' do
120 121 122 123 124 125 126 127
      let(:tempfile) { Tempfile.new }

      let(:stream) do
        described_class.new do
          tempfile.write("12345678")
          tempfile.rewind
          tempfile
        end
128 129
      end

130 131 132
      after do
        tempfile.unlink
      end
133

134 135 136 137 138 139
      it_behaves_like 'appends'
    end

    context 'when stream is ChunkedIO' do
      let(:stream) do
        described_class.new do
140
          Gitlab::Ci::Trace::ChunkedIO.new(build).tap do |chunked_io|
141 142 143 144 145 146 147
            chunked_io.write('12345678')
            chunked_io.seek(0, IO::SEEK_SET)
          end
        end
      end

      it_behaves_like 'appends'
148
    end
149 150 151
  end

  describe '#set' do
152 153 154 155 156 157 158 159 160 161
    shared_examples_for 'sets' do
      before do
        stream.set("8901")
      end

      it "overwrite content" do
        stream.seek(0)

        expect(stream.size).to eq(4)
        expect(stream.raw).to eq("8901")
162 163 164
      end
    end

165 166 167 168 169 170 171 172
    context 'when stream is StringIO' do
      let(:stream) do
        described_class.new do
          StringIO.new("12345678")
        end
      end

      it_behaves_like 'sets'
173 174
    end

175 176 177
    context 'when stream is ChunkedIO' do
      let(:stream) do
        described_class.new do
178
          Gitlab::Ci::Trace::ChunkedIO.new(build).tap do |chunked_io|
179 180 181 182 183
            chunked_io.write('12345678')
            chunked_io.seek(0, IO::SEEK_SET)
          end
        end
      end
184

185
      it_behaves_like 'sets'
186 187 188 189
    end
  end

  describe '#raw' do
190 191 192 193 194 195
    shared_examples_for 'sets' do
      it 'returns all contents if last_lines is not specified' do
        result = stream.raw

        expect(result).to eq(lines.join)
        expect(result.encoding).to eq(Encoding.default_external)
196 197
      end

198 199 200 201 202 203
      context 'limit max lines' do
        before do
          # specifying BUFFER_SIZE forces to seek backwards
          allow(described_class).to receive(:BUFFER_SIZE)
            .and_return(2)
        end
204

205 206
        it 'returns last few lines' do
          result = stream.raw(last_lines: 2)
207

208 209 210
          expect(result).to eq(lines.last(2).join)
          expect(result.encoding).to eq(Encoding.default_external)
        end
211

212 213
        it 'returns everything if trying to get too many lines' do
          result = stream.raw(last_lines: lines.size * 2)
214

215 216 217
          expect(result).to eq(lines.join)
          expect(result.encoding).to eq(Encoding.default_external)
        end
218
      end
219
    end
220

221 222
    let(:path) { __FILE__ }
    let(:lines) { File.readlines(path) }
223

224 225 226 227 228
    context 'when stream is File' do
      let(:stream) do
        described_class.new do
          File.open(path)
        end
229
      end
Shinya Maeda's avatar
Shinya Maeda committed
230 231

      it_behaves_like 'sets'
232
    end
233 234 235 236

    context 'when stream is ChunkedIO' do
      let(:stream) do
        described_class.new do
237
          Gitlab::Ci::Trace::ChunkedIO.new(build).tap do |chunked_io|
238 239 240 241 242 243 244 245
            chunked_io.write(File.binread(path))
            chunked_io.seek(0, IO::SEEK_SET)
          end
        end
      end

      it_behaves_like 'sets'
    end
246 247 248
  end

  describe '#html_with_state' do
249 250 251 252 253
    shared_examples_for 'html_with_states' do
      it 'returns html content with state' do
        result = stream.html_with_state

        expect(result.html).to eq("1234")
254 255
      end

256 257
      context 'follow-up state' do
        let!(:last_result) { stream.html_with_state }
258

259
        before do
260 261
          data_stream.seek(4, IO::SEEK_SET)
          data_stream.write("5678")
262 263
          stream.seek(0)
        end
264

265 266
        it "returns appended trace" do
          result = stream.html_with_state(last_result.state)
267

268 269 270 271 272 273 274
          expect(result.append).to be_truthy
          expect(result.html).to eq("5678")
        end
      end
    end

    context 'when stream is StringIO' do
275 276 277 278
      let(:data_stream) do
        StringIO.new("1234")
      end

279
      let(:stream) do
280
        described_class.new { data_stream }
281 282
      end

283 284
      it_behaves_like 'html_with_states'
    end
285

286
    context 'when stream is ChunkedIO' do
287 288 289 290
      let(:data_stream) do
        Gitlab::Ci::Trace::ChunkedIO.new(build).tap do |chunked_io|
          chunked_io.write("1234")
          chunked_io.seek(0, IO::SEEK_SET)
291
        end
292
      end
293

294 295 296 297
      let(:stream) do
        described_class.new { data_stream }
      end

298
      it_behaves_like 'html_with_states'
299 300 301 302
    end
  end

  describe '#html' do
303 304 305 306 307 308 309
    shared_examples_for 'htmls' do
      it "returns html" do
        expect(stream.html).to eq("12<br>34<br>56")
      end

      it "returns html for last line only" do
        expect(stream.html(last_lines: 1)).to eq("56")
310 311 312
      end
    end

313 314 315 316 317 318 319 320
    context 'when stream is StringIO' do
      let(:stream) do
        described_class.new do
          StringIO.new("12\n34\n56")
        end
      end

      it_behaves_like 'htmls'
321 322
    end

323 324 325
    context 'when stream is ChunkedIO' do
      let(:stream) do
        described_class.new do
326
          Gitlab::Ci::Trace::ChunkedIO.new(build).tap do |chunked_io|
327 328 329 330 331 332 333
            chunked_io.write("12\n34\n56")
            chunked_io.seek(0, IO::SEEK_SET)
          end
        end
      end

      it_behaves_like 'htmls'
334 335 336 337
    end
  end

  describe '#extract_coverage' do
338 339 340 341
    shared_examples_for 'extract_coverages' do
      context 'valid content & regex' do
        let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered' }
        let(:regex) { '\(\d+.\d+\%\) covered' }
342

343 344
        it { is_expected.to eq("98.29") }
      end
345

346 347 348
      context 'valid content & bad regex' do
        let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered\n' }
        let(:regex) { 'very covered' }
349

350 351
        it { is_expected.to be_nil }
      end
352

353 354 355
      context 'no coverage content & regex' do
        let(:data) { 'No coverage for today :sad:' }
        let(:regex) { '\(\d+.\d+\%\) covered' }
356

357 358
        it { is_expected.to be_nil }
      end
359

360 361 362 363 364 365 366
      context 'multiple results in content & regex' do
        let(:data) do
          <<~HEREDOC
            (98.39%) covered
            (98.29%) covered
          HEREDOC
        end
367

368
        let(:regex) { '\(\d+.\d+\%\) covered' }
369

370 371 372
        it 'returns the last matched coverage' do
          is_expected.to eq("98.29")
        end
Shinya Maeda's avatar
Shinya Maeda committed
373 374
      end

375 376 377
      context 'when BUFFER_SIZE is smaller than stream.size' do
        let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered\n' }
        let(:regex) { '\(\d+.\d+\%\) covered' }
378

379 380 381 382 383
        before do
          stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', 5)
        end

        it { is_expected.to eq("98.29") }
Shinya Maeda's avatar
Shinya Maeda committed
384
      end
385

386 387 388
      context 'when regex is multi-byte char' do
        let(:data) { '95.0 ゴッドファット\n' }
        let(:regex) { '\d+\.\d+ ゴッドファット' }
389

390 391 392 393 394
        before do
          stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', 5)
        end

        it { is_expected.to eq('95.0') }
395 396
      end

397 398 399
      context 'when BUFFER_SIZE is equal to stream.size' do
        let(:data) { 'Coverage 1033 / 1051 LOC (98.29%) covered\n' }
        let(:regex) { '\(\d+.\d+\%\) covered' }
400

401 402 403
        before do
          stub_const('Gitlab::Ci::Trace::Stream::BUFFER_SIZE', data.length)
        end
404

405
        it { is_expected.to eq("98.29") }
406 407
      end

408 409 410
      context 'using a regex capture' do
        let(:data) { 'TOTAL      9926   3489    65%' }
        let(:regex) { 'TOTAL\s+\d+\s+\d+\s+(\d{1,3}\%)' }
411

412
        it { is_expected.to eq("65") }
413 414
      end

415 416
      context 'malicious regexp' do
        let(:data) { malicious_text }
417
        let(:regex) { malicious_regexp_re2 }
418

419 420
        include_examples 'malicious regexp'
      end
421

422 423 424
      context 'multi-line data with rooted regexp' do
        let(:data) { "\n65%\n" }
        let(:regex) { '^(\d+)\%$' }
425

426 427
        it { is_expected.to eq('65') }
      end
428

429 430 431
      context 'long line' do
        let(:data) { 'a' * 80000 + '100%' + 'a' * 80000 }
        let(:regex) { '\d+\%' }
432

433 434
        it { is_expected.to eq('100') }
      end
435

436 437 438
      context 'many lines' do
        let(:data) { "foo\n" * 80000 + "100%\n" + "foo\n" * 80000 }
        let(:regex) { '\d+\%' }
439

440 441
        it { is_expected.to eq('100') }
      end
442

443 444 445
      context 'empty regex' do
        let(:data) { 'foo' }
        let(:regex) { '' }
446

447 448
        it 'skips processing' do
          expect(stream).not_to receive(:read)
449

450 451 452
          is_expected.to be_nil
        end
      end
453

454 455 456
      context 'nil regex' do
        let(:data) { 'foo' }
        let(:regex) { nil }
457

458 459
        it 'skips processing' do
          expect(stream).not_to receive(:read)
460

461 462
          is_expected.to be_nil
        end
463 464 465
      end
    end

466
    subject { stream.extract_coverage(regex) }
467

468 469 470 471 472 473 474 475 476
    context 'when stream is StringIO' do
      let(:stream) do
        described_class.new do
          StringIO.new(data)
        end
      end

      it_behaves_like 'extract_coverages'
    end
477

478 479 480
    context 'when stream is ChunkedIO' do
      let(:stream) do
        described_class.new do
481
          Gitlab::Ci::Trace::ChunkedIO.new(build).tap do |chunked_io|
482 483 484 485
            chunked_io.write(data)
            chunked_io.seek(0, IO::SEEK_SET)
          end
        end
486
      end
487 488

      it_behaves_like 'extract_coverages'
489
    end
490 491
  end
end