Back to Repositories

Testing RMagick Image Processing Implementation in CarrierWave

This test suite validates RMagick image processing functionality in CarrierWave, covering essential image manipulation operations like resizing, cropping, and format conversion. The tests ensure reliable handling of various image transformations while maintaining file integrity and format specifications.

Test Coverage Overview

The test suite provides comprehensive coverage of RMagick integration with CarrierWave, focusing on core image processing operations.

Key areas tested include:
  • Image format conversion and retention
  • Multiple resize operations (fill, fit, limit, pad)
  • Cropping and dimension manipulation
  • Color handling and transparency
  • Error handling for invalid images
  • Multi-page document processing

Implementation Analysis

The testing approach utilizes RSpec’s behavior-driven development framework with extensive use of let blocks for setup and shared resources. Tests employ mock objects and careful file handling to ensure isolated testing environments.

Key patterns include:
  • Before/after hooks for file management
  • Shared example groups for common behaviors
  • Explicit dimension verification
  • Format-specific assertions

Technical Details

Testing infrastructure includes:
  • RSpec as the testing framework
  • RMagick for image processing
  • Custom matchers for dimension verification
  • File manipulation utilities
  • I18n for error message testing
  • Mock objects for remote file testing

Best Practices Demonstrated

The test suite exemplifies several testing best practices for image processing libraries.

Notable practices include:
  • Isolated test environments with proper cleanup
  • Comprehensive edge case coverage
  • Explicit verification of image attributes
  • Internationalization testing
  • Security testing for code injection
  • Proper error handling verification

carrierwaveuploader/carrierwave

spec/processing/rmagick_spec.rb

            
require 'spec_helper'

describe CarrierWave::RMagick, :rmagick => true do

  let(:klass) { Class.new(CarrierWave::Uploader::Base) { include CarrierWave::RMagick } }
  let(:instance) { klass.new }
  let(:landscape_file_path) { file_path('landscape.jpg') }
  let(:landscape_file_copy_path) { file_path('landscape_copy.jpg') }

  before do
    FileUtils.cp(landscape_file_path, landscape_file_copy_path)
    allow(instance).to receive(:cached?).and_return true
    allow(instance).to receive(:file).and_return(CarrierWave::SanitizedFile.new(landscape_file_copy_path))
  end

  after do
    FileUtils.rm(landscape_file_copy_path) if File.exist?(file_path('landscape_copy.jpg'))
    FileUtils.rm(landscape_file_copy_path) if File.exist?(file_path('landscape_copy.jpg'))
  end

  describe '#convert' do
    it "converts the image to the given format" do
      instance.convert(:png)
      expect(instance.file.extension).to eq('png')
      expect(instance).to be_format('png')
      expect(instance.file.content_type).to eq('image/png')
    end
  end

  describe '#resize_to_fill' do
    it "resizes the image to exactly the given dimensions and maintain file type" do
      instance.resize_to_fill(200, 200)

      expect(instance).to have_dimensions(200, 200)
      expect(::Magick::Image.read(instance.current_path).first.format).to eq('JPEG')
    end

    it "resizes the image to exactly the given dimensions and maintain updated file type" do
      instance.convert('png')
      instance.resize_to_fill(200, 200)

      expect(instance).to have_dimensions(200, 200)
      expect(::Magick::Image.read(instance.current_path).first.format).to eq('PNG')
      expect(instance.file.extension).to eq('png')
    end

    it "scales up the image if it smaller than the given dimensions" do
      instance.resize_to_fill(1000, 1000)
      expect(instance).to have_dimensions(1000, 1000)
    end
  end

  describe '#resize_and_pad' do
    it "resizes the image to exactly the given dimensions and maintain file type" do
      instance.resize_and_pad(200, 200)

      expect(instance).to have_dimensions(200, 200)
      expect(::Magick::Image.read(instance.current_path).first.format).to eq('JPEG')
    end

    it "resize the image to exactly the given dimensions and maintain updated file type" do
      instance.convert('png')
      instance.resize_and_pad(200, 200)

      expect(instance).to have_dimensions(200, 200)
      expect(::Magick::Image.read(instance.current_path).first.format).to eq('PNG')
      expect(instance.file.extension).to eq('png')
    end

    it "pads with white" do
      instance.resize_and_pad(200, 200)

      color = color_of_pixel(instance.current_path, 0, 0)
      expect(color).to include('#FFFFFF')
      expect(color).not_to include('#FFFFFF00')
    end

    it "pads with transparent" do
      instance.convert('png')
      instance.resize_and_pad(200, 200, :transparent)

      color = color_of_pixel(instance.current_path, 0, 0)
      expect(color).to include('#FFFFFF00')
    end

    it "doesn't pad with transparent" do
      instance.resize_and_pad(200, 200, :transparent)
      instance.convert('png')

      color = color_of_pixel(instance.current_path, 0, 0)
      expect(color).to include('#FFFFFF')
      expect(color).not_to include('#FFFFFF00')
    end

    it "pads with given color" do
      instance.resize_and_pad(200, 200, '#888')
      color = color_of_pixel(instance.current_path, 0, 0)

      expect(color).to include('#888888')
    end

    it "scales up the image if it smaller than the given dimensions" do
      instance.resize_and_pad(1000, 1000)

      expect(instance).to have_dimensions(1000, 1000)
    end
  end

  describe '#resize_to_fit' do
    it "resizes the image to fit within the given dimensions and maintain file type" do
      instance.resize_to_fit(200, 200)

      expect(instance).to have_dimensions(200, 150)
      expect(::Magick::Image.read(instance.current_path).first.format).to eq('JPEG')
    end

    it "resize the image to fit within the given dimensions and maintain updated file type" do
      instance.convert('png')
      instance.resize_to_fit(200, 200)

      expect(instance).to have_dimensions(200, 150)
      expect(::Magick::Image.read(instance.current_path).first.format).to eq('PNG')
    end

    it "scales up the image if it smaller than the given dimensions" do
      instance.resize_to_fit(1000, 1000)

      expect(instance).to have_dimensions(1000, 750)
    end
  end

  describe '#resize_to_limit' do
    it "resizes the image to fit within the given dimensions and maintain file type" do
      instance.resize_to_limit(200, 200)

      expect(instance).to have_dimensions(200, 150)
      expect(::Magick::Image.read(instance.current_path).first.format).to eq('JPEG')
    end

    it "resizes the image to fit within the given dimensions and maintain updated file type" do
      instance.convert('png')
      instance.resize_to_limit(200, 200)

      expect(instance).to have_dimensions(200, 150)
      expect(::Magick::Image.read(instance.current_path).first.format).to eq('PNG')
      expect(instance.file.extension).to eq('png')
    end

    it "doesn't scale up the image if it smaller than the given dimensions" do
      instance.resize_to_limit(1000, 1000)
      expect(instance).to have_dimensions(640, 480)
    end
  end

  describe '#resize_to_geometry_string' do
    it "resizes the image to comply with `200x200^` Geometry String spec and maintain file type" do
      instance.resize_to_geometry_string('200x200^')

      expect(instance).to have_dimensions(267, 200)
      expect(::Magick::Image.read(instance.current_path).first.format).to eq('JPEG')
    end

    it "resizes the image to comply with `200x200^` Geometry String spec and maintain updated file type" do
      instance.convert('png')
      instance.resize_to_geometry_string('200x200^')

      expect(instance).to have_dimensions(267, 200)
      expect(::Magick::Image.read(instance.current_path).first.format).to eq('PNG')
      expect(instance.file.extension).to eq('png')
    end

    it "resizes the image to have 125% larger dimensions" do
      instance.resize_to_geometry_string('125%')
      expect(instance).to have_dimensions(800, 600)
    end

    it "resizes the image to have a given height" do
      instance.resize_to_geometry_string('x256')
      expect(instance).to have_height(256)
    end

    it "resizes the image to have a given width" do
      instance.resize_to_geometry_string('256x')
      expect(instance).to have_width(256)
    end
  end

  describe "#crop" do
    it "extracts an area defined from the left and top positions, with the given width and height" do
      instance.crop(70, 40, 500, 400)
      expect(instance).to have_dimensions(500, 400)
    end

    it "retains original image boundary if either edge of the cropping box falls outside it" do
      instance.crop(140, 80, 500, 480)
      expect(instance).to have_dimensions(500, 400)
    end
  end

  describe "#manipulate!" do
    let(:image) { ::Magick::Image.read(landscape_file_path) }

    it 'supports passing write options to RMagick' do
      allow(::Magick::Image).to receive_messages(:read => image)
      expect_any_instance_of(::Magick::Image::Info).to receive(:quality=).with(50)
      expect_any_instance_of(::Magick::Image::Info).to receive(:depth=).with(8)

      instance.manipulate! do |image, index, options|
        options[:write] = {
          :quality => 50,
          :depth => 8
        }
        image
      end
    end

    it 'supports passing read options to RMagick' do
      expect_any_instance_of(::Magick::Image::Info).to receive(:density=).with(10)
      expect_any_instance_of(::Magick::Image::Info).to receive(:size=).with("200x200")

      instance.manipulate! :read => {
        :density => 10,
          :size => "200x200"
      }
    end

    it 'shows deprecation but still accepts strings enclosed with double quotes' do
      expect_any_instance_of(::Magick::Image::Info).to receive(:size=).once.with("200x200")
      expect(CarrierWave.deprecator).to receive(:warn).with(any_args)
      instance.manipulate! :read => {:size => %{"200x200"}}
    end

    it 'shows deprecation but still accepts strings enclosed with single quotes' do
      expect_any_instance_of(::Magick::Image::Info).to receive(:size=).once.with("200x200")
      expect(CarrierWave.deprecator).to receive(:warn).with(any_args)
      instance.manipulate! :read => {:size => %{'200x200'}}
    end

    it 'does not allow arbitrary code execution' do
      expect_any_instance_of(Kernel).not_to receive(:puts)
      expect do
        instance.manipulate! :read => {
          :density => "1 }; raise; {"
        }
      end.to raise_error ArgumentError, /invalid density geometry/
    end

    it 'does not allow invocation of non-public methods' do
      Kernel.module_eval do
      private

        def foo=(value); raise; end
      end
      expect do
        instance.manipulate! :read => {
          :foo => "1"
        }
      end.to raise_error NoMethodError, /private method .foo=. called/
    end
  end

  describe "#width and #height" do
    it "returns the width and height of the image" do
      instance.resize_to_fill(200, 300)
      expect(instance.width).to eq(200)
      expect(instance.height).to eq(300)
    end
  end

  describe '#dimension_from' do
    it 'evaluates procs' do
      instance.resize_to_fill(Proc.new { 200 }, Proc.new { 200 })

      expect(instance).to have_dimensions(200, 200)
    end

    it 'evaluates procs with uploader instance' do
      width_argument = nil
      width = Proc.new do |uploader|
        width_argument = uploader
        200
      end
      height_argument = nil
      height = Proc.new do |uploader|
        height_argument = uploader
        200
      end
      instance.resize_to_fill(width, height)

      expect(instance).to have_dimensions(200, 200)
      expect(instance).to eq(width_argument)
      expect(instance).to eq(height_argument)
    end
  end

  describe "#rmagick_image" do
    it "returns a ::Magick::Image" do
      expect{instance.send(:rmagick_image)}.to_not raise_exception
      expect(instance.send(:rmagick_image).class).to eq(::Magick::Image)
    end

    context "with a remotely stored file" do
      class RemoteFile < CarrierWave::SanitizedFile # rubocop:disable Lint/ConstantDefinitionInBlock
        def initialize(local_path)
          @local_path = local_path
        end

        def current_path
          "foo/bar.jpg"
        end

        def read
          File.read @local_path
        end
      end

      before do
        allow(instance).to receive(:file).and_return(RemoteFile.new(landscape_file_copy_path))
      end

      it "returns a ::Magick::Image" do
        expect{instance.send(:rmagick_image)}.to_not raise_exception
        expect(instance.send(:rmagick_image).class).to eq(::Magick::Image)
      end
    end
  end

  describe "test errors" do
    context "invalid image file" do
      before do
        File.open(instance.current_path, 'w') { |f| f.puts "bogus" }
      end

      it "fails to process a non image file" do
        expect {instance.resize_to_limit(200, 200)}.to raise_exception(CarrierWave::ProcessingError, /^Failed to manipulate, maybe it is not an image\?/)
      end

      it "uses I18n" do
        change_locale_and_store_translations(:nl, :errors => {
          :messages => {
            :processing_error => "Kon bestand niet bewerken, misschien is het geen beeld bestand?"
          }
        }) do
          expect {instance.resize_to_limit(200, 200)}.to raise_exception(CarrierWave::ProcessingError, /^Kon bestand niet bewerken, misschien is het geen beeld bestand\?/)
        end
      end

      it "doesn't suppress errors when translation is unavailable" do
        change_locale_and_store_translations(:foo, {}) do
          expect { instance.resize_to_limit(200, 200) }.to raise_exception( CarrierWave::ProcessingError )
        end
      end

      context ":en locale is not available and enforce_available_locales is true" do
        it "doesn't suppress errors" do
          change_and_enforce_available_locales(:nl, [:nl, :foo]) do
            expect { instance.resize_to_limit(200, 200) }.to raise_exception(CarrierWave::ProcessingError)
          end
        end
      end
    end
  end

  describe "when working with frames" do
    before do
      def instance.cover
        manipulate! { |frame, index| frame if index.zero? }
      end

      klass.send :include, CarrierWave::RMagick
    end

    after { instance.instance_eval { undef cover } }

    context "with a multi-page PDF" do
      before { instance.cache! File.open(file_path("multi_page.pdf")) }

      it "successfully processes" do
        klass.process :convert => 'jpg'
        instance.process!
      end

      it "supports page specific transformations" do
        klass.process :cover
        instance.process!
      end
    end

    context "with a simple image" do
      before { instance.cache! File.open(file_path("portrait.jpg")) }

      it "allows page specific transformations" do
        klass.process :cover
        instance.process!
      end
    end
  end
end