1require 'rubygems'
2# Set up gems listed in the Gemfile.
3ENV['BUNDLE_GEMFILE'] ||= File.expand_path('Gemfile', File.dirname(__FILE__))
4require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
5Bundler.require(:default, :test) if defined?(Bundler)
6
7require File.expand_path('nginx_configuration', File.dirname(__FILE__))
8
9RSpec.configure do |config|
10  config.after(:each) do
11    NginxTestHelper::Config.delete_config_and_log_files(config_id) if has_passed?
12  end
13  config.order = "random"
14  config.run_all_when_everything_filtered = true
15end
16
17RSpec::Matchers.define :be_perceptual_equal_to do |expected, accuracy=99|
18  match do |actual|
19    (Pixmap.from_jpeg_buffer(actual) - Pixmap.from_jpeg_file(expected)).percentage_pixels_non_zero < (100 - accuracy)
20  end
21
22  failure_message do |actual|
23    "expected that #{actual} would be #{accuracy}% equals to #{expected}"
24  end
25
26  failure_message_when_negated do |actual|
27    "expected that #{actual} would be #{accuracy}% different from #{expected}"
28  end
29
30  description do
31    "be #{accuracy}% equals"
32  end
33end
34
35def image(url, headers={}, expected_status="200")
36  uri = URI.parse(nginx_address + url)
37  the_response = Net::HTTP.start(uri.host, uri.port) do |http|
38    http.read_timeout = 120
39    http.get(uri.request_uri, headers)
40  end
41
42  expect(the_response.code).to eq(expected_status)
43  if the_response.code == "200"
44    expect(the_response.header.content_type).to eq("image/jpeg")
45    the_response.body
46  else
47    expect(the_response.header.content_type).to eq("text/html")
48    nil
49  end
50end
51
52class Pixmap
53  def initialize(width, height)
54    @width = width
55    @height = height
56    @data = fill(RGBColour::WHITE)
57  end
58
59  attr_reader :width, :height
60
61  def fill(colour)
62    @data = Array.new(@width) {Array.new(@height, colour)}
63  end
64
65  def [](x, y)
66    validate_pixel(x,y)
67    @data[x][y]
68  end
69  alias_method :get_pixel, :[]
70
71  def []=(x, y, colour)
72    validate_pixel(x,y)
73    @data[x][y] = colour
74  end
75  alias_method :set_pixel, :[]=
76
77  def each_pixel
78    if block_given?
79      @height.times {|y| @width.times {|x| yield x,y }}
80    else
81      to_enum(:each_pixel)
82    end
83  end
84
85  # the difference between two images
86  def -(a_pixmap)
87    if @width != a_pixmap.width or @height != a_pixmap.height
88      raise ArgumentError, "can't compare images with different sizes"
89    end
90
91    bitmap = self.class.new(@width, @height)
92    bitmap.each_pixel do |x, y|
93      bitmap[x, y] = self[x, y] - a_pixmap[x, y]
94    end
95    bitmap
96  end
97
98  def percentage_pixels_non_zero
99    sum = 0
100    each_pixel {|x,y| sum += self[x, y].values.inject(0) {|colors_sum, val| colors_sum + val }}
101    100.0 * Float(sum) / (@width * @height * 255 * 3)
102  end
103
104  def self.from_jpeg_file(filename)
105    unless File.readable?(filename)
106      raise ArgumentError, "#{filename} does not exists or is not readable."
107    end
108
109    jpeg_to_pixmap(Jpeg.open(filename))
110  end
111
112  def self.from_jpeg_buffer(buffer)
113    jpeg_to_pixmap(Jpeg.open_buffer(buffer))
114  end
115
116  private
117
118  def self.jpeg_to_pixmap(jpeg)
119    width = jpeg.width
120    height = jpeg.height
121
122    if width < 1 || height < 1
123      raise StandardError, "file '#{filename}' does not start with the expected header"
124    end
125
126    raw_data = jpeg.raw_data
127    bitmap = self.new(width, height)
128    bitmap.each_pixel do |x,y|
129      values = raw_data[y][x]
130      if jpeg.rgb?
131        bitmap[x,y] = RGBColour.new(values[0], values[1], values[2])
132      else
133        bitmap[x,y] = RGBColour.new(values[0], values[0], values[0])
134      end
135    end
136    bitmap
137  end
138
139  def validate_pixel(x,y)
140    unless x.between?(0, @width - 1) && y.between?(0, @height - 1)
141      raise ArgumentError, "requested pixel (#{x}, #{y}) is outside dimensions of this bitmap"
142    end
143  end
144end
145
146class RGBColour
147  # Red, green and blue values must fall in the range 0..255.
148  def initialize(red, green, blue)
149    ok = [red, green, blue].inject(true) {|ret, c| ret &= c.between?(0,255)}
150    raise ArgumentError, "invalid RGB parameters: #{[red, green, blue].inspect}" unless ok
151    @red, @green, @blue = red, green, blue
152  end
153
154  attr_reader :red, :green, :blue
155  alias_method :r, :red
156  alias_method :g, :green
157  alias_method :b, :blue
158
159  def values
160    [@red, @green, @blue]
161  end
162
163  def -(a_colour)
164    self.class.new((@red - a_colour.red).abs, (@green - a_colour.green).abs, (@blue - a_colour.blue).abs)
165  end
166
167  WHITE = RGBColour.new(255, 255, 255)
168end
169