image_optim 0.16.0 → 0.17.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- MDE1MjA4Yzc2MDkwZWM5YjgwNmJhZDVmMzM5MzI5NTU2ZjY3YWViZA==
4
+ NDA2ZGQwMGNiYTI1OTgxN2IyYWY1MmEyNDg0OTgwYzQ1YmY2ODk2MQ==
5
5
  data.tar.gz: !binary |-
6
- NTMxMjc5YTI1ZmEzYzA1YTYwMWYwNWU4YzM3ODA0YTMwNDAyZDc3MQ==
7
- !binary "U0hBNTEy":
6
+ ZTdmNTAzMDYwMzRmM2ZlZmM5YTg4MDk5ZTgzNWJjZTUxZjY4OTA2YQ==
7
+ SHA512:
8
8
  metadata.gz: !binary |-
9
- YWM3ZmNmMzk0NWE4YTI0NTQxNDI1YzEyM2YxNzM3ZjlhMmM3NWIwMTQwYjE5
10
- NGIwYzJiZDk3MzhlMGJkOWQ5Y2NkYzc3OWEyMGMyZGQ5YzEyNzJkOGRhODgy
11
- NmRkNzBkMTQ0N2M3YTc3YWU2OTI0MzAxM2MyMDZmMzliMjljNDM=
9
+ NjM2NTMyODYxNDU2ZjNiOWQxYTE3NmY0NjgyMDY3ZGRmYjEzZDQzMTUwNmYy
10
+ N2IyZTdhODAxYmI0YjRjMmM5YWY5MWFlNGI0MGU3M2FmZjMzYTc1ZWQzMzhl
11
+ OWNjNGFlNzEyY2NjNDY1ODhmZTU1Mzk5MWQ5OTM4ZDQ0MmM3MWU=
12
12
  data.tar.gz: !binary |-
13
- MjEyYWRhNDA3OTZkMTM2NWNiYTc5MzI2MDEyYzQ4ZDBlYzcyNzU4MjkxODY0
14
- MTVjZjdjYjIyMWRhYTIzY2ZiMTA0ZDM3N2U2YzAyZDllM2M5Y2NkNWI5YWQw
15
- OGRhYmFmZGU5ZWVlNWIzZjYzMmJkNDI3ODM1NTg0NWY3NDdhODM=
13
+ Y2NmZjlmODI1NDAyODk0NjAwYzRmYjUzNDU0ZTllMTNhYmM4NWRkYjQ3YmMz
14
+ NDFkNDc2ZTIwNTBhOWZlNmQyYTQ2YTc0NDViYmY3NDg4Yzc0ZGNlN2JjMDA2
15
+ MjRmOGNlNjdiNTE2YWQxMGE5Mjc5NDdkYTk1YTkzNWNlZDI0MjI=
@@ -12,7 +12,7 @@ Metrics/CyclomaticComplexity:
12
12
  Max: 10
13
13
 
14
14
  Metrics/MethodLength:
15
- Max: 20
15
+ Max: 25
16
16
 
17
17
  Metrics/PerceivedComplexity:
18
18
  Max: 8
@@ -40,6 +40,7 @@ before_install:
40
40
  env:
41
41
  - PATH=~/bin:$PATH
42
42
  matrix:
43
+ fast_finish: true
43
44
  allow_failures:
44
45
  - rvm: ruby-head
45
46
  - rvm: jruby-head
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## unreleased
4
4
 
5
+ ## v0.17.0 (2014-10-04)
6
+
7
+ * Use pure ruby detection of bin path [@toy](https://github.com/toy)
8
+ * Fail if version of bin can't be detected [#39](https://github.com/toy/image_optim/issues/39) [@toy](https://github.com/toy)
9
+ * Check path in `XXX_BIN` to exist, be a file and be executable [@toy](https://github.com/toy)
10
+ * `image_optim --info` to perform initialization with verbose output without running optimizations [@toy](https://github.com/toy)
11
+ * Changeable config paths [@toy](https://github.com/toy)
12
+
5
13
  ## v0.16.0 (2014-09-12)
6
14
 
7
15
  * Wrote this ChangeLog [#62](https://github.com/toy/image_optim/issues/62) [@toy](https://github.com/toy)
@@ -220,11 +220,13 @@ Image optimization can be time consuming, so depending on your deployment proces
220
220
 
221
221
  ## Configuration
222
222
 
223
- Configuration in YAML format will be read and prepanded to options from two paths:
223
+ Configuration in YAML format will be read and prepended to options from two paths:
224
224
 
225
225
  * `$XDG_CONFIG_HOME/image_optim.yml` (by default `~/.config/image_optim.yml`)
226
226
  * `.image_optim.yml` in current working directory
227
227
 
228
+ Paths can be changed using `:config_paths` option and `--config-paths` argument.
229
+
228
230
  Example configuration:
229
231
 
230
232
  ```yaml
@@ -18,11 +18,18 @@ option_parser = OptionParser.new do |op|
18
18
  | #{op.program_name} [options] image_path …
19
19
  |
20
20
  |Configuration will be read and prepanded to options from two paths:
21
- | #{ImageOptim::Config::GLOBAL_CONFIG_PATH}
22
- | #{ImageOptim::Config::LOCAL_CONFIG_PATH}
21
+ | #{ImageOptim::Config::GLOBAL_PATH}
22
+ | #{ImageOptim::Config::LOCAL_PATH}
23
23
  |
24
24
  TEXT
25
25
 
26
+ op.on('--config-paths PATH1,PATH2', Array, 'Config paths to use instead of '\
27
+ 'default ones') do |paths|
28
+ options[:config_paths] = paths
29
+ end
30
+
31
+ op.separator nil
32
+
26
33
  op.on('-r', '-R', '--recursive', 'Recursively scan directories '\
27
34
  'for images') do |recursive|
28
35
  options[:recursive] = recursive
@@ -107,33 +114,42 @@ option_parser = OptionParser.new do |op|
107
114
  op.separator nil
108
115
  op.separator ' Common options:'
109
116
 
110
- op.on('-v', '--verbose', 'Verbose output') do |verbose|
111
- options[:verbose] = verbose
117
+ op.on('-v', '--verbose', 'Verbose output') do
118
+ options[:verbose] = true
112
119
  end
113
120
 
114
- op.on_tail('-h', '--help', 'Show full help') do
121
+ op.on_tail('-h', '--help', 'Show help and exit') do
115
122
  puts op.help
116
123
  exit
117
124
  end
118
125
 
119
- op.on_tail('--version', 'Show version') do
126
+ op.on_tail('--version', 'Show version and exit') do
120
127
  puts ImageOptim.version
121
128
  exit
122
129
  end
130
+
131
+ op.on_tail('--info', 'Show environment info and exit') do
132
+ options[:verbose] = true
133
+ options[:only_info] = true
134
+ end
123
135
  end
124
136
 
125
137
  begin
126
138
  args = ARGV.dup
127
139
 
128
- # assume -v to be request to print version if it is the only argument
140
+ # assume -v to be a request to print version if it is the only argument
129
141
  args = %w[--version] if args == %w[-v]
130
142
 
131
143
  option_parser.parse!(args)
132
144
  if options[:verbose]
133
145
  $stderr.puts ImageOptim.full_version
134
146
  end
135
- unless ImageOptim::Runner.run!(args, options)
136
- abort
147
+
148
+ only_info = options.delete(:only_info)
149
+ runner = ImageOptim::Runner.new(options)
150
+ unless only_info
151
+ abort 'specify paths to optimize' if args.empty?
152
+ abort unless runner.run!(args)
137
153
  end
138
154
  rescue OptionParser::ParseError => e
139
155
  abort "#{e}\n\n#{option_parser.help}"
@@ -2,7 +2,7 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = 'image_optim'
5
- s.version = '0.16.0'
5
+ s.version = '0.17.0'
6
6
  s.summary = %q{Optimize (lossless compress) images (jpeg, png, gif, svg) using external utilities (advpng, gifsicle, jpegoptim, jpegtran, optipng, pngcrush, pngout, pngquant, svgo)}
7
7
  s.homepage = "http://github.com/toy/#{s.name}"
8
8
  s.authors = ['Ivan Kuchin']
@@ -1,59 +1,53 @@
1
1
  require 'thread'
2
2
  require 'fspath'
3
- require 'image_optim/bin_resolver/simple_version'
4
- require 'image_optim/bin_resolver/comparable_condition'
3
+ require 'image_optim/bin_resolver/error'
4
+ require 'image_optim/bin_resolver/bin'
5
5
 
6
6
  class ImageOptim
7
7
  # Handles resolving binaries and checking versions
8
8
  #
9
- # If there is an environment variable XXX_BIN when resolbing xxx, then a
9
+ # If there is an environment variable XXX_BIN when resolving xxx, then a
10
10
  # symlink to binary will be created in a temporary directory which will be
11
11
  # added to PATH
12
12
  class BinResolver
13
- class Error < StandardError; end
14
13
  class BinNotFound < Error; end
15
- class BadBinVersion < Error; end
16
-
17
- # Holds name and version of an executable
18
- class Bin
19
- attr_reader :name, :version
20
- def initialize(name, version)
21
- @name = name
22
- @version = version && SimpleVersion.new(version)
23
- end
24
-
25
- def to_s
26
- "#{@name} #{@version || '-'}"
27
- end
28
- end
29
14
 
15
+ # Directory for symlinks to bins if XXX_BIN was used
30
16
  attr_reader :dir
17
+
31
18
  def initialize(image_optim)
32
19
  @image_optim = image_optim
33
20
  @bins = {}
34
21
  @lock = Mutex.new
35
22
  end
36
23
 
24
+ # Binary resolving: create symlink if there is XXX_BIN environment variable,
25
+ # build Bin with full path, check binary version
37
26
  def resolve!(name)
38
27
  name = name.to_sym
39
28
 
40
29
  resolving(name) do
41
- bin = Bin.new(name, version(name)) if resolve?(name)
30
+ path = symlink_custom_bin!(name) || full_path(name)
31
+ bin = Bin.new(name, path) if path
32
+
42
33
  if bin && @image_optim.verbose
43
34
  $stderr << "Resolved #{bin}\n"
44
35
  end
36
+
45
37
  @bins[name] = bin
46
38
  end
47
39
 
48
40
  if @bins[name]
49
- check!(@bins[name])
41
+ @bins[name].check!
50
42
  else
51
43
  fail BinNotFound, "`#{name}` not found"
52
44
  end
53
45
  end
54
46
 
47
+ # Path to vendor at root of image_optim
55
48
  VENDOR_PATH = File.expand_path('../../../vendor', __FILE__)
56
49
 
50
+ # Prepand `dir` and append `VENDOR_PATH` to `PATH` from environment
57
51
  def env_path
58
52
  [dir, ENV['PATH'], VENDOR_PATH].compact.join(':')
59
53
  end
@@ -73,6 +67,7 @@ class ImageOptim
73
67
 
74
68
  private
75
69
 
70
+ # Double-checked locking
76
71
  def resolving(name)
77
72
  return if @bins.include?(name)
78
73
  @lock.synchronize do
@@ -80,71 +75,41 @@ class ImageOptim
80
75
  end
81
76
  end
82
77
 
83
- def resolve?(name)
84
- if (path = ENV["#{name}_bin".upcase])
85
- unless @dir
86
- @dir = FSPath.temp_dir
87
- at_exit{ FileUtils.remove_entry_secure @dir }
88
- end
89
- symlink = @dir / name
90
- symlink.make_symlink(File.expand_path(path))
78
+ # Check path in XXX_BIN to exist, be a file and be executable and symlink to
79
+ # dir as name
80
+ def symlink_custom_bin!(name)
81
+ env_name = "#{name}_bin".upcase
82
+ path = ENV[env_name]
83
+ return unless path
84
+ path = File.expand_path(path)
85
+ desc = "`#{path}` specified in #{env_name}"
86
+ fail "#{desc} doesn\'t exist" unless File.exist?(path)
87
+ fail "#{desc} is not a file" unless File.file?(path)
88
+ fail "#{desc} is not executable" unless File.executable?(path)
89
+ if @image_optim.verbose
90
+ $stderr << "Custom path for #{name} specified in #{env_name}: #{path}\n"
91
91
  end
92
- accessible?(name)
93
- end
94
-
95
- def accessible?(name)
96
- !!version(name)
97
- end
98
-
99
- def version(name)
100
- case name.to_sym
101
- when :advpng, :gifsicle, :jpegoptim, :optipng, :pngquant
102
- capture_output("#{name} --version 2> /dev/null")[/\d+(\.\d+){1,}/]
103
- when :svgo
104
- capture_output("#{name} --version 2>&1")[/\d+(\.\d+){1,}/]
105
- when :jhead
106
- capture_output("#{name} -V 2> /dev/null")[/\d+(\.\d+){1,}/]
107
- when :jpegtran
108
- capture_output("#{name} -v - 2>&1")[/version (\d+\S*)/, 1]
109
- when :pngcrush
110
- capture_output("#{name} -version 2>&1")[/\d+(\.\d+){1,}/]
111
- when :pngout
112
- date_regexp = /[A-Z][a-z]{2} (?: |\d)\d \d{4}/
113
- date_str = capture_output("#{name} 2>&1")[date_regexp]
114
- Date.parse(date_str).strftime('%Y%m%d') if date_str
115
- when :jpegrescan
116
- # jpegrescan has no version so just check presence
117
- capture_output("command -v #{name}")['jpegrescan']
118
- else
119
- fail "getting `#{name}` version is not defined"
92
+ unless @dir
93
+ @dir = FSPath.temp_dir
94
+ at_exit{ FileUtils.remove_entry_secure @dir }
120
95
  end
96
+ symlink = @dir / name
97
+ symlink.make_symlink(path)
98
+ path
121
99
  end
122
100
 
123
- def check!(bin)
124
- is = ComparableCondition.is
125
- case bin.name
126
- when :pngcrush
127
- case bin.version
128
- when c = is.between?('1.7.60', '1.7.65')
129
- fail BadBinVersion, "`#{bin}` (#{c}) is known to produce broken pngs"
130
- end
131
- when :advpng
132
- case bin.version
133
- when c = is < '1.17'
134
- warn "Note that `#{bin}` (#{c}) does not use zopfli"
135
- end
136
- when :pngquant
137
- case bin.version
138
- when c = is < '2.0'
139
- fail BadBinVersion, "`#{bin}` (#{c}) is not supported"
140
- when c = is < '2.1'
141
- warn "Note that `#{bin}` (#{c}) may be lossy even with quality `100-`"
101
+ # Return full path to bin or null
102
+ # based on http://stackoverflow.com/a/5471032/96823
103
+ def full_path(name)
104
+ # PATHEXT is needed only for windows
105
+ exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
106
+ ENV['PATH'].to_s.split(File::PATH_SEPARATOR).each do |dir|
107
+ exts.each do |ext|
108
+ path = File.expand_path("#{name}#{ext}", dir)
109
+ return path if File.file?(path) && File.executable?(path)
142
110
  end
143
111
  end
144
- end
145
-
146
- def capture_output(command)
147
- `env PATH=#{env_path.shellescape} #{command}`
112
+ nil
148
113
  end
149
114
  end
150
115
  end
@@ -0,0 +1,84 @@
1
+ require 'image_optim/bin_resolver/error'
2
+ require 'image_optim/bin_resolver/simple_version'
3
+ require 'image_optim/bin_resolver/comparable_condition'
4
+
5
+ class ImageOptim
6
+ class BinResolver
7
+ # Holds bin name and path, gets version
8
+ class Bin
9
+ class BadVersion < Error; end
10
+
11
+ attr_reader :name, :path, :version
12
+ def initialize(name, path)
13
+ @name = name.to_sym
14
+ @path = path
15
+ @version = detect_version
16
+ end
17
+
18
+ def to_s
19
+ "#{name} #{version || '?'} at #{path}"
20
+ end
21
+
22
+ # Fail or warn if version is known to misbehave depending on severity
23
+ def check!
24
+ unless version
25
+ fail BadVersion, "didn't get version of #{name} at #{path}"
26
+ end
27
+
28
+ is = ComparableCondition.is
29
+ case name
30
+ when :pngcrush
31
+ case version
32
+ when c = is.between?('1.7.60', '1.7.65')
33
+ fail BadVersion, "#{self} (#{c}) is known to produce broken pngs"
34
+ end
35
+ when :advpng
36
+ case version
37
+ when c = is < '1.17'
38
+ warn "WARN: #{self} (#{c}) does not use zopfli"
39
+ end
40
+ when :pngquant
41
+ case version
42
+ when c = is < '2.0'
43
+ fail BadVersion, "#{self} (#{c}) is not supported"
44
+ when c = is < '2.1'
45
+ warn "WARN: #{self} (#{c}) may be lossy even with quality `100-`"
46
+ end
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ # Wrap version_string with SimpleVersion
53
+ def detect_version
54
+ str = version_string
55
+ str && SimpleVersion.new(str)
56
+ end
57
+
58
+ # Getting version of bin, will fail for an unknown name
59
+ def version_string
60
+ case name
61
+ when :advpng, :gifsicle, :jpegoptim, :optipng, :pngquant
62
+ `#{path.shellescape} --version 2> /dev/null`[/\d+(\.\d+){1,}/]
63
+ when :svgo
64
+ `#{path.shellescape} --version 2>&1`[/\d+(\.\d+){1,}/]
65
+ when :jhead
66
+ `#{path.shellescape} -V 2> /dev/null`[/\d+(\.\d+){1,}/]
67
+ when :jpegtran
68
+ `#{path.shellescape} -v - 2>&1`[/version (\d+\S*)/, 1]
69
+ when :pngcrush
70
+ `#{path.shellescape} -version 2>&1`[/\d+(\.\d+){1,}/]
71
+ when :pngout
72
+ date_regexp = /[A-Z][a-z]{2} (?: |\d)\d \d{4}/
73
+ date_str = `#{path.shellescape} 2>&1`[date_regexp]
74
+ Date.parse(date_str).strftime('%Y%m%d') if date_str
75
+ when :jpegrescan
76
+ # jpegrescan has no version so just check presence
77
+ path && '-'
78
+ else
79
+ fail "getting `#{name}` version is not defined"
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,6 @@
1
+ class ImageOptim
2
+ class BinResolver
3
+ # Base error during bin resolving
4
+ class Error < StandardError; end
5
+ end
6
+ end
@@ -10,26 +10,15 @@ class ImageOptim
10
10
  class Config
11
11
  include OptionHelpers
12
12
 
13
- CONFIG_HOME = ENV['XDG_CONFIG_HOME'] || '~/.config'
14
- GLOBAL_CONFIG_PATH = File.join(CONFIG_HOME, 'image_optim.yml')
15
- LOCAL_CONFIG_PATH = './.image_optim.yml'
13
+ GLOBAL_PATH = begin
14
+ File.join(ENV['XDG_CONFIG_HOME'] || '~/.config', 'image_optim.yml')
15
+ end
16
+ LOCAL_PATH = './.image_optim.yml'
16
17
 
17
18
  class << self
18
- # Read config at GLOBAL_CONFIG_PATH if it exists, warn if anything is
19
- # wrong
20
- def global
21
- read(GLOBAL_CONFIG_PATH)
22
- end
23
-
24
- # Read config at LOCAL_CONFIG_PATH if it exists, warn if anything is
25
- # wrong
26
- def local
27
- read(LOCAL_CONFIG_PATH)
28
- end
29
-
30
- private
31
-
32
- def read(path)
19
+ # Read options at path: expand path (warn on failure), return {} if file
20
+ # does not exist, read yaml, check if it is a Hash, deep symbolise keys
21
+ def read_options(path)
33
22
  begin
34
23
  full_path = File.expand_path(path)
35
24
  rescue ArgumentError => e
@@ -49,11 +38,13 @@ class ImageOptim
49
38
  end
50
39
 
51
40
  def initialize(options)
52
- @options = [
53
- Config.global,
54
- Config.local,
55
- HashHelpers.deep_symbolise_keys(options),
56
- ].reduce do |memo, hash|
41
+ config_paths = options.delete(:config_paths) || [GLOBAL_PATH, LOCAL_PATH]
42
+ config_paths = Array(config_paths)
43
+
44
+ to_merge = config_paths.map{ |path| self.class.read_options(path) }
45
+ to_merge << HashHelpers.deep_symbolise_keys(options)
46
+
47
+ @options = to_merge.reduce do |memo, hash|
57
48
  HashHelpers.deep_merge(memo, hash)
58
49
  end
59
50
  @used = Set.new
@@ -44,8 +44,7 @@ class ImageOptim
44
44
  end
45
45
  end
46
46
 
47
- def initialize(args, options)
48
- fail 'specify paths to optimize' if args.empty?
47
+ def initialize(options)
49
48
  options = HashHelpers.deep_symbolise_keys(options)
50
49
  @recursive = options.delete(:recursive)
51
50
  @exclude_dir_globs, @exclude_file_globs = %w[dir file].map do |type|
@@ -53,14 +52,14 @@ class ImageOptim
53
52
  GlobHelpers.expand_braces(glob)
54
53
  end
55
54
  @image_optim = ImageOptim.new(options)
56
- @to_optimize = find_to_optimize(args)
57
55
  end
58
56
 
59
- def run!
60
- unless @to_optimize.empty?
57
+ def run!(args)
58
+ to_optimize = find_to_optimize(args)
59
+ unless to_optimize.empty?
61
60
  results = Results.new
62
61
 
63
- optimize_images!.each do |original, optimized|
62
+ optimize_images!(to_optimize).each do |original, optimized|
64
63
  results.add(original, optimized)
65
64
  end
66
65
 
@@ -70,15 +69,11 @@ class ImageOptim
70
69
  !@warnings
71
70
  end
72
71
 
73
- def self.run!(args, options)
74
- new(args, options).run!
75
- end
76
-
77
72
  private
78
73
 
79
- def optimize_images!(&block)
74
+ def optimize_images!(to_optimize, &block)
80
75
  @image_optim.
81
- optimize_images!(@to_optimize.with_progress('optimizing'), &block)
76
+ optimize_images!(to_optimize.with_progress('optimizing'), &block)
82
77
  end
83
78
 
84
79
  def find_to_optimize(paths)
@@ -1,5 +1,6 @@
1
1
  # encoding: UTF-8
2
2
 
3
+ require 'image_optim/bin_resolver/error'
3
4
  require 'image_optim/configuration_error'
4
5
  require 'image_optim/option_definition'
5
6
  require 'image_optim/option_helpers'
@@ -10,50 +10,85 @@ ensure
10
10
  end
11
11
 
12
12
  describe ImageOptim::BinResolver do
13
+ BinResolver = ImageOptim::BinResolver
14
+ Bin = BinResolver::Bin
15
+ SimpleVersion = BinResolver::SimpleVersion
16
+
13
17
  let(:image_optim){ double(:image_optim, :verbose => false) }
14
- let(:resolver){ ImageOptim::BinResolver.new(image_optim) }
18
+ let(:resolver){ BinResolver.new(image_optim) }
19
+
20
+ describe :full_path do
21
+ def full_path(name)
22
+ resolver.instance_eval{ full_path(name) }
23
+ end
24
+
25
+ def command_v(name)
26
+ path = `sh -c 'command -v #{name}' 2> /dev/null`.strip
27
+ path unless path.empty?
28
+ end
29
+
30
+ it 'should find binary in path' do
31
+ with_env 'PATH', 'bin' do
32
+ expect(full_path('image_optim')).
33
+ to eq(File.expand_path('bin/image_optim'))
34
+ end
35
+ end
36
+
37
+ it 'should return nil on failure' do
38
+ with_env 'PATH', 'lib' do
39
+ expect(full_path('image_optim')).to be_nil
40
+ end
41
+ end
42
+
43
+ %w[ls sh which bash image_optim should_not_exist].each do |name|
44
+ it "should return same path as `command -v` for #{name}" do
45
+ expect(full_path(name)).to eq(command_v(name))
46
+ end
47
+ end
48
+ end
15
49
 
16
50
  it 'should resolve bin in path' do
17
51
  with_env 'LS_BIN', nil do
18
- allow(resolver).to receive(:version).with(:ls).and_return('xxx')
19
- expect(resolver).to receive(:accessible?).with(:ls).once.and_return(true)
20
52
  expect(FSPath).not_to receive(:temp_dir)
53
+ expect(resolver).to receive(:full_path).with(:ls).and_return('/bin/ls')
54
+ bin = double
55
+ expect(Bin).to receive(:new).with(:ls, '/bin/ls').and_return(bin)
56
+ expect(bin).to receive(:check!).exactly(5).times
21
57
 
22
58
  5.times do
23
59
  resolver.resolve!(:ls)
24
60
  end
25
61
  expect(resolver.env_path).to eq([
26
62
  ENV['PATH'],
27
- ImageOptim::BinResolver::VENDOR_PATH,
63
+ BinResolver::VENDOR_PATH,
28
64
  ].join(':'))
29
65
  end
30
66
  end
31
67
 
32
- it 'should fail to resolve unknown bin' do
68
+ it 'should raise on failure to resolve bin' do
33
69
  with_env 'LS_BIN', nil do
34
70
  expect(FSPath).not_to receive(:temp_dir)
71
+ expect(resolver).to receive(:full_path).with(:ls).and_return(nil)
72
+ expect(Bin).not_to receive(:new)
35
73
 
36
74
  5.times do
37
75
  expect do
38
76
  resolver.resolve!(:ls)
39
- end.to raise_error RuntimeError
77
+ end.to raise_error BinResolver::BinNotFound
40
78
  end
41
79
  expect(resolver.env_path).to eq([
42
80
  ENV['PATH'],
43
- ImageOptim::BinResolver::VENDOR_PATH,
81
+ BinResolver::VENDOR_PATH,
44
82
  ].join(':'))
45
83
  end
46
84
  end
47
85
 
48
86
  it 'should resolve bin specified in ENV' do
49
- path = 'some/path/image_optim2.3.4'
87
+ path = 'bin/image_optim'
50
88
  with_env 'IMAGE_OPTIM_BIN', path do
51
89
  tmpdir = double(:tmpdir, :to_str => 'tmpdir')
52
90
  symlink = double(:symlink)
53
91
 
54
- allow(resolver).to receive(:version).with(:image_optim).and_return('xxx')
55
- expect(resolver).to receive(:accessible?).
56
- with(:image_optim).once.and_return(true)
57
92
  expect(FSPath).to receive(:temp_dir).
58
93
  once.and_return(tmpdir)
59
94
  expect(tmpdir).to receive(:/).
@@ -61,6 +96,12 @@ describe ImageOptim::BinResolver do
61
96
  expect(symlink).to receive(:make_symlink).
62
97
  with(File.expand_path(path)).once
63
98
 
99
+ expect(resolver).not_to receive(:full_path)
100
+ bin = double
101
+ expect(Bin).to receive(:new).
102
+ with(:image_optim, File.expand_path(path)).and_return(bin)
103
+ expect(bin).to receive(:check!).exactly(5).times
104
+
64
105
  at_exit_blocks = []
65
106
  expect(resolver).to receive(:at_exit).once do |&block|
66
107
  at_exit_blocks.unshift(block)
@@ -72,7 +113,7 @@ describe ImageOptim::BinResolver do
72
113
  expect(resolver.env_path).to eq([
73
114
  tmpdir,
74
115
  ENV['PATH'],
75
- ImageOptim::BinResolver::VENDOR_PATH,
116
+ BinResolver::VENDOR_PATH,
76
117
  ].join(':'))
77
118
 
78
119
  expect(FileUtils).to receive(:remove_entry_secure).with(tmpdir)
@@ -80,85 +121,71 @@ describe ImageOptim::BinResolver do
80
121
  end
81
122
  end
82
123
 
83
- it 'should raise on failure to resolve bin' do
84
- with_env 'PATH', nil do
85
- expect(FSPath).not_to receive(:temp_dir)
86
-
87
- 5.times do
88
- expect do
89
- resolver.resolve!(:jpegtran)
90
- end.to raise_error ImageOptim::BinResolver::BinNotFound
124
+ {
125
+ 'some/path/should_not_exist_bin' => 'doesn\'t exist',
126
+ '.' => 'is not a file',
127
+ __FILE__ => 'is not executable',
128
+ }.each do |path, error_message|
129
+ it "should raise when bin specified in ENV #{error_message}" do
130
+ with_env 'IMAGE_OPTIM_BIN', path do
131
+ expect(FSPath).not_to receive(:temp_dir)
132
+ expect(resolver).not_to receive(:at_exit)
133
+
134
+ 5.times do
135
+ expect do
136
+ resolver.resolve!(:image_optim)
137
+ end.to raise_error RuntimeError, /#{Regexp.escape(error_message)}/
138
+ end
139
+ expect(resolver.env_path).to eq([
140
+ ENV['PATH'],
141
+ BinResolver::VENDOR_PATH,
142
+ ].join(':'))
91
143
  end
92
- expect(resolver.env_path).to eq(ImageOptim::BinResolver::VENDOR_PATH)
93
144
  end
94
145
  end
95
146
 
96
- it 'should raise on failure to resolve bin specified in ENV' do
97
- path = 'some/path/should_not_exist_bin'
98
- with_env 'SHOULD_NOT_EXIST_BIN', path do
99
- tmpdir = double(:tmpdir, :to_str => 'tmpdir')
100
- symlink = double(:symlink)
101
-
102
- expect(resolver).to receive(:accessible?).
103
- with(:should_not_exist).once.and_return(false)
104
- expect(FSPath).to receive(:temp_dir).
105
- once.and_return(tmpdir)
106
- expect(tmpdir).to receive(:/).
107
- with(:should_not_exist).once.and_return(symlink)
108
- expect(symlink).to receive(:make_symlink).
109
- with(File.expand_path(path)).once
110
-
111
- at_exit_blocks = []
112
- expect(resolver).to receive(:at_exit).once do |&block|
113
- at_exit_blocks.unshift(block)
147
+ it 'should resolve bin only once, but check every time' do
148
+ with_env 'LS_BIN', nil do
149
+ expect(resolver).to receive(:full_path).once.with(:ls) do
150
+ sleep 0.1
151
+ '/bin/ls'
114
152
  end
153
+ bin = double
154
+ expect(Bin).to receive(:new).once.with(:ls, '/bin/ls').and_return(bin)
115
155
 
116
- 5.times do
117
- expect do
118
- resolver.resolve!(:should_not_exist)
119
- end.to raise_error ImageOptim::BinResolver::BinNotFound
120
- end
121
- expect(resolver.env_path).to eq([
122
- tmpdir,
123
- ENV['PATH'],
124
- ImageOptim::BinResolver::VENDOR_PATH,
125
- ].join(':'))
126
-
127
- expect(FileUtils).to receive(:remove_entry_secure).with(tmpdir)
128
- at_exit_blocks.each(&:call)
129
- end
130
- end
131
-
132
- it 'should resolve bin only once' do
133
- with_env 'LS_BIN', nil do
134
- allow(resolver).to receive(:version).with(:ls).and_return('xxx')
135
- expect(resolver).to receive(:resolve?).once.with(:ls){ sleep 0.1; true }
156
+ check_count = 0
157
+ mutex = Mutex.new
158
+ allow(bin).to receive(:check!){ mutex.synchronize{ check_count += 1 } }
136
159
 
137
160
  10.times.map do
138
161
  Thread.new do
139
162
  resolver.resolve!(:ls)
140
163
  end
141
164
  end.each(&:join)
165
+
166
+ expect(check_count).to eq(10)
167
+ end
168
+ end
169
+
170
+ it 'should raise if did not got bin version' do
171
+ bin = Bin.new(:pngcrush, '/bin/pngcrush')
172
+ allow(bin).to receive(:version).and_return(nil)
173
+
174
+ 5.times do
175
+ expect do
176
+ bin.check!
177
+ end.to raise_error Bin::BadVersion
142
178
  end
143
179
  end
144
180
 
145
181
  it 'should raise on detection of problematic version' do
146
- with_env 'PNGCRUSH_BIN', nil do
147
- expect(resolver).to receive(:accessible?).
148
- with(:pngcrush).once.and_return(true)
149
- expect(resolver).to receive(:version).
150
- with(:pngcrush).once.and_return('1.7.60')
151
- expect(FSPath).not_to receive(:temp_dir)
182
+ bin = Bin.new(:pngcrush, '/bin/pngcrush')
183
+ allow(bin).to receive(:version).and_return(SimpleVersion.new('1.7.60'))
152
184
 
153
- 5.times do
154
- expect do
155
- resolver.resolve!(:pngcrush)
156
- end.to raise_error ImageOptim::BinResolver::BadBinVersion
157
- end
158
- expect(resolver.env_path).to eq([
159
- ENV['PATH'],
160
- ImageOptim::BinResolver::VENDOR_PATH,
161
- ].join(':'))
185
+ 5.times do
186
+ expect do
187
+ bin.check!
188
+ end.to raise_error Bin::BadVersion
162
189
  end
163
190
  end
164
191
  end
@@ -3,21 +3,20 @@ require 'rspec'
3
3
  require 'image_optim/config'
4
4
 
5
5
  describe ImageOptim::Config do
6
- Config = ImageOptim::Config
7
-
8
- before do
9
- allow(Config).to receive(:global).and_return({})
10
- allow(Config).to receive(:local).and_return({})
11
- end
6
+ IOConfig = ImageOptim::Config
12
7
 
13
8
  describe 'assert_no_unused_options!' do
9
+ before do
10
+ allow(IOConfig).to receive(:read_options).and_return({})
11
+ end
12
+
14
13
  it 'should not raise when no unused options' do
15
- config = Config.new({})
14
+ config = IOConfig.new({})
16
15
  config.assert_no_unused_options!
17
16
  end
18
17
 
19
18
  it 'should raise when there are unused options' do
20
- config = Config.new(:unused => true)
19
+ config = IOConfig.new(:unused => true)
21
20
  expect do
22
21
  config.assert_no_unused_options!
23
22
  end.to raise_error(ImageOptim::ConfigurationError)
@@ -25,41 +24,53 @@ describe ImageOptim::Config do
25
24
  end
26
25
 
27
26
  describe 'nice' do
27
+ before do
28
+ allow(IOConfig).to receive(:read_options).and_return({})
29
+ end
30
+
28
31
  it 'should be 10 by default' do
29
- config = Config.new({})
32
+ config = IOConfig.new({})
30
33
  expect(config.nice).to eq(10)
31
34
  end
32
35
 
33
36
  it 'should be 0 if disabled' do
34
- config = Config.new(:nice => false)
37
+ config = IOConfig.new(:nice => false)
35
38
  expect(config.nice).to eq(0)
36
39
  end
37
40
 
38
41
  it 'should convert value to number' do
39
- config = Config.new(:nice => '13')
42
+ config = IOConfig.new(:nice => '13')
40
43
  expect(config.nice).to eq(13)
41
44
  end
42
45
  end
43
46
 
44
47
  describe 'threads' do
48
+ before do
49
+ allow(IOConfig).to receive(:read_options).and_return({})
50
+ end
51
+
45
52
  it 'should be processor_count by default' do
46
- config = Config.new({})
53
+ config = IOConfig.new({})
47
54
  allow(config).to receive(:processor_count).and_return(13)
48
55
  expect(config.threads).to eq(13)
49
56
  end
50
57
 
51
58
  it 'should be 1 if disabled' do
52
- config = Config.new(:threads => false)
59
+ config = IOConfig.new(:threads => false)
53
60
  expect(config.threads).to eq(1)
54
61
  end
55
62
 
56
63
  it 'should convert value to number' do
57
- config = Config.new(:threads => '616')
64
+ config = IOConfig.new(:threads => '616')
58
65
  expect(config.threads).to eq(616)
59
66
  end
60
67
  end
61
68
 
62
69
  describe 'for_worker' do
70
+ before do
71
+ allow(IOConfig).to receive(:read_options).and_return({})
72
+ end
73
+
63
74
  Abc = Class.new do
64
75
  def self.bin_sym
65
76
  :abc
@@ -71,80 +82,103 @@ describe ImageOptim::Config do
71
82
  end
72
83
 
73
84
  it 'should return empty hash by default' do
74
- config = Config.new({})
85
+ config = IOConfig.new({})
75
86
  expect(config.for_worker(Abc)).to eq({})
76
87
  end
77
88
 
78
89
  it 'should return passed hash' do
79
- config = Config.new(:abc => {:option => true})
90
+ config = IOConfig.new(:abc => {:option => true})
80
91
  expect(config.for_worker(Abc)).to eq(:option => true)
81
92
  end
82
93
 
83
94
  it 'should return passed false' do
84
- config = Config.new(:abc => false)
95
+ config = IOConfig.new(:abc => false)
85
96
  expect(config.for_worker(Abc)).to eq(false)
86
97
  end
87
98
 
88
- it 'should raise on unknown optino' do
89
- config = Config.new(:abc => 13)
99
+ it 'should raise on unknown option' do
100
+ config = IOConfig.new(:abc => 13)
90
101
  expect do
91
102
  config.for_worker(Abc)
92
103
  end.to raise_error(ImageOptim::ConfigurationError)
93
104
  end
94
105
  end
95
106
 
96
- describe 'class methods' do
97
- before do
98
- allow(Config).to receive(:global).and_call_original
99
- allow(Config).to receive(:local).and_call_original
107
+ describe 'config' do
108
+ it 'should read options from default locations' do
109
+ expect(IOConfig).to receive(:read_options).
110
+ with(IOConfig::GLOBAL_PATH).and_return(:a => 1, :b => 2, :c => 3)
111
+ expect(IOConfig).to receive(:read_options).
112
+ with(IOConfig::LOCAL_PATH).and_return(:a => 10, :b => 20)
113
+
114
+ config = IOConfig.new(:a => 100)
115
+ expect(config.get!(:a)).to eq(100)
116
+ expect(config.get!(:b)).to eq(20)
117
+ expect(config.get!(:c)).to eq(3)
118
+ config.assert_no_unused_options!
100
119
  end
101
120
 
102
- describe 'global' do
103
- it 'should call read with GLOBAL_CONFIG_PATH' do
104
- expect(Config).to receive(:read).
105
- with(Config::GLOBAL_CONFIG_PATH).and_return(:config => true)
121
+ it 'should not read options with empty config_paths' do
122
+ expect(IOConfig).not_to receive(:read_options)
106
123
 
107
- expect(Config.global).to eq(:config => true)
108
- end
124
+ config = IOConfig.new(:config_paths => [])
125
+ config.assert_no_unused_options!
109
126
  end
110
127
 
111
- describe 'local' do
112
- it 'should call read with LOCAL_CONFIG_PATH' do
113
- expect(Config).to receive(:read).
114
- with(Config::LOCAL_CONFIG_PATH).and_return(:config => true)
128
+ it 'should read options from specified paths' do
129
+ expect(IOConfig).to receive(:read_options).
130
+ with('/etc/image_optim.yml').and_return(:a => 1, :b => 2, :c => 3)
131
+ expect(IOConfig).to receive(:read_options).
132
+ with('config/image_optim.yml').and_return(:a => 10, :b => 20)
115
133
 
116
- expect(Config.local).to eq(:config => true)
117
- end
134
+ config = IOConfig.new(:a => 100, :config_paths => %w[
135
+ /etc/image_optim.yml
136
+ config/image_optim.yml
137
+ ])
138
+ expect(config.get!(:a)).to eq(100)
139
+ expect(config.get!(:b)).to eq(20)
140
+ expect(config.get!(:c)).to eq(3)
141
+ config.assert_no_unused_options!
118
142
  end
119
143
 
120
- describe 'read' do
144
+ it 'should convert config_paths to array' do
145
+ expect(IOConfig).to receive(:read_options).
146
+ with('config/image_optim.yml').and_return({})
147
+
148
+ config = IOConfig.new(:config_paths => 'config/image_optim.yml')
149
+ config.assert_no_unused_options!
150
+ end
151
+ end
152
+
153
+ describe 'class methods' do
154
+ describe 'read_options' do
121
155
  let(:path){ double(:path) }
122
156
  let(:full_path){ double(:full_path) }
123
157
 
124
158
  it 'should warn if expand path fails' do
125
- expect(Config).to receive(:warn)
159
+ expect(IOConfig).to receive(:warn)
126
160
  expect(File).to receive(:expand_path).
127
161
  with(path).and_raise(ArgumentError)
128
162
  expect(File).not_to receive(:file?)
129
163
 
130
- expect(Config.send(:read, path)).to eq({})
164
+ expect(IOConfig.read_options(path)).to eq({})
131
165
  end
132
166
 
133
167
  it 'should return empty hash if path is not a file' do
134
- expect(Config).not_to receive(:warn)
168
+ expect(IOConfig).not_to receive(:warn)
135
169
  expect(File).to receive(:expand_path).
136
170
  with(path).and_return(full_path)
137
171
  expect(File).to receive(:file?).
138
172
  with(full_path).and_return(false)
139
173
 
140
- expect(Config.send(:read, path)).to eq({})
174
+ expect(IOConfig.read_options(path)).to eq({})
141
175
  end
142
176
 
143
177
  it 'should return hash with deep symbolised keys from reader' do
144
178
  stringified = {'config' => {'this' => true}}
145
179
  symbolized = {:config => {:this => true}}
146
180
 
147
- expect(Config).not_to receive(:warn)
181
+ expect(IOConfig).not_to receive(:warn)
148
182
  expect(File).to receive(:expand_path).
149
183
  with(path).and_return(full_path)
150
184
  expect(File).to receive(:file?).
@@ -152,11 +186,11 @@ describe ImageOptim::Config do
152
186
  expect(YAML).to receive(:load_file).
153
187
  with(full_path).and_return(stringified)
154
188
 
155
- expect(Config.send(:read, path)).to eq(symbolized)
189
+ expect(IOConfig.read_options(path)).to eq(symbolized)
156
190
  end
157
191
 
158
192
  it 'should warn and return an empty hash if reader returns non hash' do
159
- expect(Config).to receive(:warn)
193
+ expect(IOConfig).to receive(:warn)
160
194
  expect(File).to receive(:expand_path).
161
195
  with(path).and_return(full_path)
162
196
  expect(File).to receive(:file?).
@@ -164,11 +198,11 @@ describe ImageOptim::Config do
164
198
  expect(YAML).to receive(:load_file).
165
199
  with(full_path).and_return([:config])
166
200
 
167
- expect(Config.send(:read, path)).to eq({})
201
+ expect(IOConfig.read_options(path)).to eq({})
168
202
  end
169
203
 
170
204
  it 'should warn and return an empty hash if reader raises exception' do
171
- expect(Config).to receive(:warn)
205
+ expect(IOConfig).to receive(:warn)
172
206
  expect(File).to receive(:expand_path).
173
207
  with(path).and_return(full_path)
174
208
  expect(File).to receive(:file?).
@@ -176,7 +210,7 @@ describe ImageOptim::Config do
176
210
  expect(YAML).to receive(:load_file).
177
211
  with(full_path).and_raise
178
212
 
179
- expect(Config.send(:read, path)).to eq({})
213
+ expect(IOConfig.read_options(path)).to eq({})
180
214
  end
181
215
  end
182
216
  end
@@ -9,8 +9,8 @@ describe ImageOptim::ImagePath do
9
9
  it 'should return ImagePath for string' do
10
10
  path = 'a'
11
11
 
12
- expect(ImagePath.convert(path)).to be_a(ImageOptim::ImagePath)
13
- expect(ImagePath.convert(path)).to eq(ImageOptim::ImagePath.new(path))
12
+ expect(ImagePath.convert(path)).to be_a(ImagePath)
13
+ expect(ImagePath.convert(path)).to eq(ImagePath.new(path))
14
14
 
15
15
  expect(ImagePath.convert(path)).not_to eq(path)
16
16
  expect(ImagePath.convert(path)).not_to be(path)
@@ -19,18 +19,18 @@ describe ImageOptim::ImagePath do
19
19
  it 'should return ImagePath for Pathname' do
20
20
  pathname = Pathname.new('a')
21
21
 
22
- expect(ImagePath.convert(pathname)).to be_a(ImageOptim::ImagePath)
23
- expect(ImagePath.convert(pathname)).to eq(ImageOptim::ImagePath.new(pathname))
22
+ expect(ImagePath.convert(pathname)).to be_a(ImagePath)
23
+ expect(ImagePath.convert(pathname)).to eq(ImagePath.new(pathname))
24
24
 
25
25
  expect(ImagePath.convert(pathname)).to eq(pathname)
26
26
  expect(ImagePath.convert(pathname)).not_to be(pathname)
27
27
  end
28
28
 
29
29
  it 'should return same instance for ImagePath' do
30
- image_path = ImageOptim::ImagePath.new('a')
30
+ image_path = ImagePath.new('a')
31
31
 
32
- expect(ImagePath.convert(image_path)).to be_a(ImageOptim::ImagePath)
33
- expect(ImagePath.convert(image_path)).to eq(ImageOptim::ImagePath.new(image_path))
32
+ expect(ImagePath.convert(image_path)).to be_a(ImagePath)
33
+ expect(ImagePath.convert(image_path)).to eq(ImagePath.new(image_path))
34
34
 
35
35
  expect(ImagePath.convert(image_path)).to eq(image_path)
36
36
  expect(ImagePath.convert(image_path)).to be(image_path)
@@ -76,17 +76,11 @@ describe ImageOptim do
76
76
  end
77
77
 
78
78
  describe 'worker' do
79
- image_optim = ImageOptim.new
80
-
81
79
  base_options = Hash[ImageOptim::Worker.klasses.map do |klass|
82
80
  [klass.bin_sym, false]
83
81
  end]
84
82
 
85
- real_workers = ImageOptim::Worker.klasses.reject do |klass|
86
- klass.new(image_optim, {}).image_formats.empty?
87
- end
88
-
89
- real_workers.each do |worker_klass|
83
+ ImageOptim::Worker.klasses.each do |worker_klass|
90
84
  describe worker_klass.bin_sym do
91
85
  it 'should optimize at least one test image' do
92
86
  options = base_options.merge(worker_klass.bin_sym => true)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: image_optim
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.16.0
4
+ version: 0.17.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivan Kuchin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-09-12 00:00:00.000000000 Z
11
+ date: 2014-10-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: fspath
@@ -145,7 +145,9 @@ files:
145
145
  - image_optim.gemspec
146
146
  - lib/image_optim.rb
147
147
  - lib/image_optim/bin_resolver.rb
148
+ - lib/image_optim/bin_resolver/bin.rb
148
149
  - lib/image_optim/bin_resolver/comparable_condition.rb
150
+ - lib/image_optim/bin_resolver/error.rb
149
151
  - lib/image_optim/bin_resolver/simple_version.rb
150
152
  - lib/image_optim/config.rb
151
153
  - lib/image_optim/configuration_error.rb
@@ -227,7 +229,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
227
229
  version: '0'
228
230
  requirements: []
229
231
  rubyforge_project: image_optim
230
- rubygems_version: 2.0.3
232
+ rubygems_version: 2.4.1
231
233
  signing_key:
232
234
  specification_version: 4
233
235
  summary: Optimize (lossless compress) images (jpeg, png, gif, svg) using external
@@ -267,4 +269,3 @@ test_files:
267
269
  - spec/images/transparency1.png
268
270
  - spec/images/transparency2.png
269
271
  - spec/images/vergroessert.jpg
270
- has_rdoc: