summaryrefslogtreecommitdiff
path: root/lib/chef/formatters/error_inspectors/compile_error_inspector.rb
blob: fe418ed4850e9789ff639a8e7f3569019961e1ed (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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#--
# Author:: Daniel DeLeo (<dan@opscode.com>)
# Copyright:: Copyright (c) 2012 Opscode, Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

class Chef
  module Formatters
    module ErrorInspectors

      # == CompileErrorInspector
      # Wraps exceptions that occur during the compile phase of a Chef run and
      # tries to find the code responsible for the error.
      class CompileErrorInspector

        attr_reader :path
        attr_reader :exception

        def initialize(path, exception)
          @path, @exception = path, exception
          @backtrace_lines_in_cookbooks = nil
          @file_lines = nil
          @culprit_backtrace_entry = nil
          @culprit_line = nil
        end

        def add_explanation(error_description)
          error_description.section(exception.class.name, exception.message)

          if found_error_in_cookbooks?
            traceback = filtered_bt.map {|line| "  #{line}"}.join("\n")
            error_description.section("Cookbook Trace:", traceback)
            error_description.section("Relevant File Content:", context)
          end

          if exception_message_modifying_frozen?
            msg = <<-MESSAGE
            Chef calls the freeze method on certain ruby objects to prevent
            pollution across multiple instances. Specifically, resource
            properties have frozen default values to avoid modifying the
            property for all instances of a resource. Try modifying the
            particular instance variable or using an instance accessor instead.
            MESSAGE

            error_description.section("Additional information:", msg.gsub(/^ {6}/, ''))
          end
        end

        def context
          context_lines = []
          context_lines << "#{culprit_file}:\n\n"
          Range.new(display_lower_bound, display_upper_bound).each do |i|
            line_nr = (i + 1).to_s.rjust(3)
            indicator = (i + 1) == culprit_line ? ">> " : ":  "
            context_lines << "#{line_nr}#{indicator}#{file_lines[i]}"
          end
          context_lines.join("")
        end

        def display_lower_bound
          lower = (culprit_line - 8)
          lower = 0 if lower < 0
          lower
        end

        def display_upper_bound
          upper = (culprit_line + 8)
          upper = file_lines.size if upper > file_lines.size
          upper
        end

        def file_lines
          @file_lines ||= IO.readlines(culprit_file)
        end

        def culprit_backtrace_entry
          @culprit_backtrace_entry ||= begin
             bt_entry = filtered_bt.first
             Chef::Log.debug("backtrace entry for compile error: '#{bt_entry}'")
             bt_entry
          end
        end

        def culprit_line
          @culprit_line ||= begin
            line_number = culprit_backtrace_entry[/^(?:.\:)?[^:]+:([\d]+)/,1].to_i
            Chef::Log.debug("Line number of compile error: '#{line_number}'")
            line_number
          end
        end

        def culprit_file
          @culprit_file ||= culprit_backtrace_entry[/^((?:.\:)?[^:]+):([\d]+)/,1]
        end

        def filtered_bt
          backtrace_lines_in_cookbooks.count > 0 ? backtrace_lines_in_cookbooks : exception.backtrace
        end

        def found_error_in_cookbooks?
          !backtrace_lines_in_cookbooks.empty?
        end

        def backtrace_lines_in_cookbooks
          @backtrace_lines_in_cookbooks ||=
            begin
              filters = Array(Chef::Config.cookbook_path).map {|p| /^#{Regexp.escape(p)}/i }
              r = exception.backtrace.select {|line| filters.any? {|filter| line =~ filter }}
              Chef::Log.debug("filtered backtrace of compile error: #{r.join(",")}")
              r
            end
        end

        def exception_message_modifying_frozen?
          exception.message.include?("can't modify frozen")
        end

      end

    end
  end
end