image_optim 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: