summaryrefslogtreecommitdiff
path: root/lib/gitlab/chat/output.rb
blob: 411b1555a7d67212d79654d6bb7ab8679bd0315e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# frozen_string_literal: true

module Gitlab
  module Chat
    # Class for gathering and formatting the output of a `Ci::Build`.
    class Output
      attr_reader :build

      MissingBuildSectionError = Class.new(StandardError)

      # The primary trace section to look for.
      PRIMARY_SECTION = 'chat_reply'

      # The backup trace section in case the primary one could not be found.
      FALLBACK_SECTION = 'build_script'

      # build - The `Ci::Build` to obtain the output from.
      def initialize(build)
        @build = build
      end

      # Returns a `String` containing the output of the build.
      #
      # The output _does not_ include the command that was executed.
      def to_s
        offset, length = read_offset_and_length

        trace.read do |stream|
          stream.seek(offset)

          output = stream
            .stream
            .read(length)
            .force_encoding(Encoding.default_external)

          without_executed_command_line(output)
        end
      end

      # Returns the offset to seek to and the number of bytes to read relative
      # to the offset.
      def read_offset_and_length
        section = find_build_trace_section(PRIMARY_SECTION) ||
          find_build_trace_section(FALLBACK_SECTION)

        unless section
          raise(
            MissingBuildSectionError,
            "The build_script trace section could not be found for build #{build.id}"
          )
        end

        length = section[:byte_end] - section[:byte_start]

        [section[:byte_start], length]
      end

      # Removes the line containing the executed command from the build output.
      #
      # output - A `String` containing the output of a trace section.
      def without_executed_command_line(output)
        # If `output.split("\n")` produces an empty Array then the slicing that
        # follows it will produce a nil. For example:
        #
        #     "\n".split("\n")        # => []
        #     "\n".split("\n")[1..-1] # => nil
        #
        # To work around this we only "join" if we're given an Array.
        if (converted = output.split("\n")[1..-1])
          converted.join("\n")
        else
          ''
        end
      end

      # Returns the trace section for the given name, or `nil` if the section
      # could not be found.
      #
      # name - The name of the trace section to find.
      def find_build_trace_section(name)
        trace_sections.find { |s| s[:name] == name }
      end

      def trace_sections
        @trace_sections ||= trace.extract_sections
      end

      def trace
        @trace ||= build.trace
      end
    end
  end
end