image_optim 0.31.3 → 0.32.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.
@@ -34,8 +34,8 @@ class ImageOptim
34
34
  def help
35
35
  text = super
36
36
 
37
- # reserve one column
38
- columns = terminal_columns - 1
37
+ # reserve one column and limit to 120
38
+ columns = [terminal_columns - 1, 120].min
39
39
  # 1 for distance between summary and description
40
40
  # 2 for additional indent
41
41
  wrapped_indent = summary_indent + (' ' * (summary_width + 1 + 2))
@@ -43,20 +43,7 @@ class ImageOptim
43
43
  # don't try to wrap if there is too little space for description
44
44
  return text if wrapped_width < 20
45
45
 
46
- wrapped = ''.dup
47
- text.split("\n").each do |line|
48
- if line.length <= columns
49
- wrapped << line << "\n"
50
- else
51
- indented = line =~ /^\s/
52
- wrapped << line.slice!(wrap_regex(columns)) << "\n"
53
- line.scan(wrap_regex(wrapped_width)) do |part|
54
- wrapped << wrapped_indent if indented
55
- wrapped << part << "\n"
56
- end
57
- end
58
- end
59
- wrapped
46
+ wrapped_text(text, wrapped_width, wrapped_indent, columns)
60
47
  end
61
48
 
62
49
  private
@@ -66,6 +53,27 @@ class ImageOptim
66
53
  stty_columns ? stty_columns.to_i : `tput cols`.to_i
67
54
  end
68
55
 
56
+ def wrapped_text(text, wrapped_width, wrapped_indent, columns)
57
+ wrapped = []
58
+ text.split("\n").each do |line|
59
+ if line.length <= columns
60
+ wrapped << line << "\n"
61
+ else
62
+ wrapped << line.slice!(wrap_regex(columns)).rstrip << "\n"
63
+ if line =~ /^\s/
64
+ line.scan(wrap_regex(wrapped_width)) do |part|
65
+ wrapped << wrapped_indent << part.rstrip << "\n"
66
+ end
67
+ else
68
+ line.scan(wrap_regex(columns)) do |part|
69
+ wrapped << part.rstrip << "\n"
70
+ end
71
+ end
72
+ end
73
+ end
74
+ wrapped.join(nil)
75
+ end
76
+
69
77
  def wrap_regex(width)
70
78
  /.*?.{1,#{width}}(?:\s|\z)/
71
79
  end
@@ -96,30 +104,25 @@ ImageOptim::Runner::OptionParser::DEFINE = proc do |op, options|
96
104
  |
97
105
  TEXT
98
106
 
99
- op.on('--config-paths PATH1,PATH2', Array, 'Config paths to use instead of ' \
100
- 'default ones') do |paths|
107
+ op.on('--config-paths PATH1,PATH2', Array, 'Config paths to use instead of default ones') do |paths|
101
108
  options[:config_paths] = paths
102
109
  end
103
110
 
104
111
  op.separator nil
105
112
 
106
- op.on('-r', '-R', '--recursive', 'Recursively scan directories ' \
107
- 'for images') do |recursive|
113
+ op.on('-r', '-R', '--recursive', 'Recursively scan directories for images') do |recursive|
108
114
  options[:recursive] = recursive
109
115
  end
110
116
 
111
- op.on("--exclude-dir 'GLOB'", 'Glob for excluding directories ' \
112
- '(defaults to .*)') do |glob|
117
+ op.on("--exclude-dir 'GLOB'", 'Glob for excluding directories (defaults to .*)') do |glob|
113
118
  options[:exclude_dir_glob] = glob
114
119
  end
115
120
 
116
- op.on("--exclude-file 'GLOB'", 'Glob for excluding files ' \
117
- '(defaults to .*)') do |glob|
121
+ op.on("--exclude-file 'GLOB'", 'Glob for excluding files (defaults to .*)') do |glob|
118
122
  options[:exclude_file_glob] = glob
119
123
  end
120
124
 
121
- op.on("--exclude 'GLOB'", 'Set glob for excluding both directories and ' \
122
- 'files') do |glob|
125
+ op.on("--exclude 'GLOB'", 'Set glob for excluding both directories and files') do |glob|
123
126
  options[:exclude_file_glob] = options[:exclude_dir_glob] = glob
124
127
  end
125
128
 
@@ -129,41 +132,51 @@ ImageOptim::Runner::OptionParser::DEFINE = proc do |op, options|
129
132
  options[:show_progress] = show_progress
130
133
  end
131
134
 
132
- op.on('--[no-]threads N', Integer, 'Number of threads or disable ' \
133
- '(defaults to number of processors)') do |threads|
135
+ op.on('--[no-]threads N', Integer, 'Number of threads or disable (defaults to number of processors)') do |threads|
134
136
  options[:threads] = threads
135
137
  end
136
138
 
137
- op.on('--[no-]nice N', Integer, 'Nice level, priority of all used tools ' \
138
- 'with higher value meaning lower priority, in range -20..19, negative ' \
139
- 'values can be set only if run by root user (defaults to 10)') do |nice|
139
+ op.on(
140
+ '--[no-]nice N',
141
+ Integer,
142
+ 'Nice level, priority of all used tools with higher value meaning lower priority, in range -20..19, negative ' \
143
+ 'values can be set only if run by root user (defaults to 10)'
144
+ ) do |nice|
140
145
  options[:nice] = nice
141
146
  end
142
147
 
143
- op.on('--[no-]pack', 'Require image_optim_pack or disable it, ' \
144
- 'by default image_optim_pack will be used if available, ' \
145
- 'will turn on skip-missing-workers unless explicitly disabled') do |pack|
148
+ op.on(
149
+ '--[no-]pack',
150
+ 'Require image_optim_pack or disable it, by default image_optim_pack will be used if available, will turn on ' \
151
+ 'skip-missing-workers unless explicitly disabled'
152
+ ) do |pack|
146
153
  options[:pack] = pack
147
154
  end
148
155
 
156
+ op.separator nil
157
+ op.on(
158
+ '--benchmark TYPE',
159
+ [:isolated],
160
+ 'Run benchmarks, to compare tools without modifying images. `isolated` is the only supported type so far.'
161
+ ) do |benchmark|
162
+ options[:benchmark] = benchmark
163
+ end
164
+
149
165
  op.separator nil
150
166
  op.separator ' Caching:'
151
167
 
152
- op.on('--cache-dir DIR', 'Cache optimized images ' \
153
- 'into the specified directory') do |cache_dir|
168
+ op.on('--cache-dir DIR', 'Cache optimized images into the specified directory') do |cache_dir|
154
169
  options[:cache_dir] = cache_dir
155
170
  end
156
171
 
157
- op.on('--cache-worker-digests', 'Cache worker digests ' \
158
- '(updating workers invalidates cache)') do |cache_worker_digests|
172
+ op.on('--cache-worker-digests', 'Cache worker digests (updating workers invalidates cache)') do |cache_worker_digests|
159
173
  options[:cache_worker_digests] = cache_worker_digests
160
174
  end
161
175
 
162
176
  op.separator nil
163
177
  op.separator ' Disabling workers:'
164
178
 
165
- op.on('--[no-]skip-missing-workers', 'Skip workers with missing or ' \
166
- 'problematic binaries') do |skip|
179
+ op.on('--[no-]skip-missing-workers', 'Skip workers with missing or problematic binaries') do |skip|
167
180
  options[:skip_missing_workers] = skip
168
181
  end
169
182
 
@@ -177,8 +190,7 @@ ImageOptim::Runner::OptionParser::DEFINE = proc do |op, options|
177
190
  op.separator nil
178
191
  op.separator ' Worker options:'
179
192
 
180
- op.on('--allow-lossy', 'Allow lossy workers and ' \
181
- 'optimizations') do |allow_lossy|
193
+ op.on('--allow-lossy', 'Allow lossy workers and optimizations') do |allow_lossy|
182
194
  options[:allow_lossy] = allow_lossy
183
195
  end
184
196
 
@@ -229,9 +241,12 @@ ImageOptim::Runner::OptionParser::DEFINE = proc do |op, options|
229
241
  op.separator nil
230
242
  op.separator ' Common options:'
231
243
 
232
- op.on_tail('-v', '--verbose', 'Verbose output (show global and worker ' \
233
- 'config, binary resolution log, information about each tool invocation, ' \
234
- 'backtrace of exception)') do
244
+ op.on_tail(
245
+ '-v',
246
+ '--verbose',
247
+ 'Verbose output (show global and worker config, binary resolution log, information about each tool invocation, ' \
248
+ 'backtrace of exception)'
249
+ ) do
235
250
  options[:verbose] = true
236
251
  end
237
252
 
@@ -45,6 +45,43 @@ class ImageOptim
45
45
  end
46
46
  end
47
47
 
48
+ # files, elapsed, kb saved, kb/s
49
+ class BenchmarkResults
50
+ def initialize
51
+ @all = []
52
+ end
53
+
54
+ def add(rows)
55
+ @all.concat(rows)
56
+ end
57
+
58
+ def print
59
+ if @all.empty?
60
+ puts 'nothing to report'
61
+ return
62
+ end
63
+
64
+ report = @all.group_by(&:worker).map do |name, results|
65
+ kb = (results.sum(&:bytes) / 1024.0)
66
+ elapsed = results.sum(&:elapsed)
67
+ {
68
+ 'name' => name,
69
+ 'files' => results.length,
70
+ 'elapsed' => elapsed,
71
+ 'kb saved' => kb,
72
+ 'kb/s' => (kb / elapsed),
73
+ }
74
+ end
75
+
76
+ report = report.sort_by do |row|
77
+ [-row['kb/s'], row['name']]
78
+ end
79
+
80
+ puts "\nBENCHMARK RESULTS\n\n"
81
+ Table.new(report).write($stdout)
82
+ end
83
+ end
84
+
48
85
  def initialize(options)
49
86
  options = HashHelpers.deep_symbolise_keys(options)
50
87
  @recursive = options.delete(:recursive)
@@ -53,19 +90,40 @@ class ImageOptim
53
90
  glob = options.delete(:"exclude_#{type}_glob") || '.*'
54
91
  GlobHelpers.expand_braces(glob)
55
92
  end
93
+
94
+ # --benchmark
95
+ @benchmark = options.delete(:benchmark)
96
+ if @benchmark
97
+ unless options[:threads].nil?
98
+ warning '--benchmark ignores --threads'
99
+ options[:threads] = 1 # for consistency
100
+ end
101
+ if options[:timeout]
102
+ warning '--benchmark ignores --timeout'
103
+ end
104
+ end
105
+
56
106
  @image_optim = ImageOptim.new(options)
57
107
  end
58
108
 
59
- def run!(args)
109
+ def run!(args) # rubocop:disable Naming/PredicateMethod
60
110
  to_optimize = find_to_optimize(args)
61
111
  unless to_optimize.empty?
62
- results = Results.new
112
+ if @benchmark
113
+ benchmark_results = BenchmarkResults.new
114
+ benchmark_images(to_optimize).each do |_original, rows| # rubocop:disable Style/HashEachMethods
115
+ benchmark_results.add(rows)
116
+ end
117
+ benchmark_results.print
118
+ else
119
+ results = Results.new
63
120
 
64
- optimize_images!(to_optimize).each do |original, optimized|
65
- results.add(original, optimized)
66
- end
121
+ optimize_images!(to_optimize).each do |original, optimized|
122
+ results.add(original, optimized)
123
+ end
67
124
 
68
- results.print
125
+ results.print
126
+ end
69
127
  end
70
128
 
71
129
  !@warnings
@@ -73,6 +131,11 @@ class ImageOptim
73
131
 
74
132
  private
75
133
 
134
+ def benchmark_images(to_optimize, &block)
135
+ to_optimize = to_optimize.with_progress('benchmarking') if @progress
136
+ @image_optim.benchmark_images(to_optimize, &block)
137
+ end
138
+
76
139
  def optimize_images!(to_optimize, &block)
77
140
  to_optimize = to_optimize.with_progress('optimizing') if @progress
78
141
  @image_optim.optimize_images!(to_optimize, &block)
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ImageOptim
4
+ # Handy class for pretty printing a table in the terminal. This is very simple, switch to Terminal
5
+ # Table, Table Tennis or similar if we need more.
6
+ class Table
7
+ attr_reader :rows
8
+
9
+ def initialize(rows)
10
+ @rows = rows
11
+ end
12
+
13
+ def write(io)
14
+ io.puts render_row(columns)
15
+ io.puts render_sep
16
+ rows.each do |row|
17
+ io.puts render_row(row.values)
18
+ end
19
+ end
20
+
21
+ protected
22
+
23
+ # array of column names
24
+ def columns
25
+ @columns ||= rows.first.keys
26
+ end
27
+
28
+ # should columns be justified left or right?
29
+ def justs
30
+ @justs ||= columns.map do |col|
31
+ rows.first[col].is_a?(Numeric) ? :rjust : :ljust
32
+ end
33
+ end
34
+
35
+ # max width of each column
36
+ def widths
37
+ @widths ||= columns.map do |col|
38
+ values = rows.map{ |row| fmt(row[col]) }
39
+ ([col] + values).map(&:length).max
40
+ end
41
+ end
42
+
43
+ # render an array of row values
44
+ def render_row(values)
45
+ values.zip(justs, widths).map do |value, just, width|
46
+ fmt(value).send(just, width)
47
+ end.join(' ')
48
+ end
49
+
50
+ # render a separator line
51
+ def render_sep
52
+ render_row(widths.map{ |width| '-' * width })
53
+ end
54
+
55
+ # format one cell value
56
+ def fmt(value)
57
+ if value.is_a?(Float)
58
+ format('%0.3f', value)
59
+ else
60
+ value.to_s
61
+ end
62
+ end
63
+ end
64
+ end
@@ -1,19 +1,41 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'image_optim/option_helpers'
3
4
  require 'image_optim/worker'
5
+ require 'fspath'
4
6
 
5
7
  class ImageOptim
6
8
  class Worker
7
9
  # https://github.com/svg/svgo
8
10
  class Svgo < Worker
11
+ PLUGIN_NAME_R = /\A[a-zA-Z]+\z/.freeze
12
+
9
13
  DISABLE_PLUGINS_OPTION =
10
14
  option(:disable_plugins, [], 'List of plugins to disable') do |v|
11
- Array(v).map(&:to_s)
15
+ parse_plugin_names(v)
12
16
  end
13
17
 
14
18
  ENABLE_PLUGINS_OPTION =
15
19
  option(:enable_plugins, [], 'List of plugins to enable') do |v|
16
- Array(v).map(&:to_s)
20
+ parse_plugin_names(v)
21
+ end
22
+
23
+ ALLOW_LOSSY_OPTION =
24
+ option(:allow_lossy, false, 'Allow precision option'){ |v| !!v }
25
+
26
+ PRECISION_OPTION =
27
+ option(:precision, 3, 'Number of digits in the fractional part ' \
28
+ '`0`..`20`, ignored in default/lossless mode') \
29
+ do |v, opt_def|
30
+ if allow_lossy
31
+ OptionHelpers.limit_with_range(v.to_i, 0..20)
32
+ else
33
+ if v != opt_def.default
34
+ warn "#{self.class.bin_sym} #{opt_def.name} #{v} ignored " \
35
+ 'in default/lossless mode'
36
+ end
37
+ opt_def.default
38
+ end
17
39
  end
18
40
 
19
41
  def optimize(src, dst, options = {})
@@ -21,14 +43,57 @@ class ImageOptim
21
43
  --input #{src}
22
44
  --output #{dst}
23
45
  ]
24
- disable_plugins.each do |plugin_name|
25
- args.unshift "--disable=#{plugin_name}"
26
- end
27
- enable_plugins.each do |plugin_name|
28
- args.unshift "--enable=#{plugin_name}"
46
+ if resolve_bin!(:svgo).version >= '2.0.0'
47
+ unless disable_plugins.empty? && enable_plugins.empty?
48
+ config_file = plugins_config_file
49
+ args.unshift "--config=#{config_file.path}"
50
+ end
51
+ else
52
+ disable_plugins.each do |plugin_name|
53
+ args.unshift "--disable=#{plugin_name}"
54
+ end
55
+ enable_plugins.each do |plugin_name|
56
+ args.unshift "--enable=#{plugin_name}"
57
+ end
29
58
  end
59
+ args.unshift "--precision=#{precision}" if allow_lossy
30
60
  execute(:svgo, args, options) && optimized?(src, dst)
31
61
  end
62
+
63
+ private
64
+
65
+ def parse_plugin_names(value)
66
+ Array(value).map(&:to_s).select do |name|
67
+ if name =~ PLUGIN_NAME_R
68
+ true
69
+ else
70
+ warn "Doesn't look like svgo plugin name: #{name}"
71
+ end
72
+ end
73
+ end
74
+
75
+ def plugins_config_file
76
+ @plugins_config_file ||= FSPath.temp_file(%w[image_optim .js]).tap do |config_file|
77
+ config_file.puts 'export default {'
78
+ config_file.puts ' plugins: ['
79
+ config_file.puts ' {'
80
+ config_file.puts ' name: \'preset-default\','
81
+ config_file.puts ' params: {'
82
+ config_file.puts ' overrides: {'
83
+ disable_plugins.each do |plugin_name|
84
+ config_file.puts " #{plugin_name}: false,"
85
+ end
86
+ config_file.puts ' }'
87
+ config_file.puts ' }'
88
+ config_file.puts ' },'
89
+ enable_plugins.each do |plugin_name|
90
+ config_file.puts " '#{plugin_name}',"
91
+ end
92
+ config_file.puts ' ]'
93
+ config_file.puts '};'
94
+ config_file.close
95
+ end
96
+ end
32
97
  end
33
98
  end
34
99
  end
data/lib/image_optim.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'image_optim/benchmark_result'
3
4
  require 'image_optim/bin_resolver'
4
5
  require 'image_optim/cache'
5
6
  require 'image_optim/config'
@@ -8,6 +9,7 @@ require 'image_optim/handler'
8
9
  require 'image_optim/image_meta'
9
10
  require 'image_optim/optimized_path'
10
11
  require 'image_optim/path'
12
+ require 'image_optim/table'
11
13
  require 'image_optim/timer'
12
14
  require 'image_optim/worker'
13
15
  require 'in_threads'
@@ -162,6 +164,22 @@ class ImageOptim
162
164
  end
163
165
  end
164
166
 
167
+ def benchmark_image(original)
168
+ src = Path.convert(original)
169
+ return unless (workers = workers_for_image(src))
170
+
171
+ dst = src.temp_path
172
+ begin
173
+ workers.map do |worker|
174
+ start = ElapsedTime.now
175
+ worker.optimize(src, dst)
176
+ BenchmarkResult.new(src, dst, ElapsedTime.now - start, worker)
177
+ end
178
+ ensure
179
+ dst.unlink
180
+ end
181
+ end
182
+
165
183
  # Optimize multiple images
166
184
  # if block given yields path and result for each image and returns array of
167
185
  # yield results
@@ -186,6 +204,10 @@ class ImageOptim
186
204
  run_method_for(datas, :optimize_image_data, &block)
187
205
  end
188
206
 
207
+ def benchmark_images(paths, &block)
208
+ run_method_for(paths, :benchmark_image, &block)
209
+ end
210
+
189
211
  class << self
190
212
  # Optimization methods with default options
191
213
  def method_missing(method, *args, &block)
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euxo pipefail
4
+
5
+ ruby <<'RUBY'
6
+ short_version = RUBY_VERSION.to_f
7
+
8
+ gemrc_path = File.expand_path('~/.gemrc')
9
+ unless File.exist?(gemrc_path)
10
+ File.open(gemrc_path, 'w') do |f|
11
+ if short_version < 2.0
12
+ f.puts 'gem: --no-ri --no-rdoc'
13
+ else
14
+ f.puts 'gem: --no-document'
15
+ end
16
+ end
17
+ end
18
+
19
+ def sh(*args)
20
+ abort unless system(*args)
21
+ end
22
+
23
+ case
24
+ when short_version < 2.3
25
+ sh 'curl --output rubygems-update.gem https://rubygems.org/downloads/rubygems-update-2.7.11.gem'
26
+ sh 'gem install --local rubygems-update.gem'
27
+ sh 'update_rubygems'
28
+ File.unlink(`which bundle`.strip)
29
+ sh 'curl --output bundler.gem https://rubygems.org/downloads/bundler-1.17.3.gem'
30
+ sh 'gem install --local bundler.gem'
31
+ when short_version < 2.6
32
+ sh 'gem update --system 3.3.27'
33
+ sh 'gem install bundler --version 2.3.27'
34
+ when short_version < 3.0
35
+ sh 'gem update --system 3.4.22'
36
+ sh 'gem install bundler --version 2.4.22'
37
+ when short_version < 3.1
38
+ sh 'gem update --system 3.5.23'
39
+ sh 'gem install bundler --version 2.5.23'
40
+ when short_version < 3.2
41
+ sh 'gem update --system 3.6.9'
42
+ sh 'gem install bundler --version 2.6.9'
43
+ else
44
+ sh 'gem update --system'
45
+ sh 'gem install bundler'
46
+ end
47
+ RUBY
48
+
49
+ gem -v
50
+ bundle -v
@@ -51,8 +51,21 @@ def update_readme(text)
51
51
  end
52
52
 
53
53
  readme = File.read(README_FILE)
54
- if (readme = update_readme(readme))
55
- File.write(README_FILE, readme)
54
+ program_name = File.basename($PROGRAM_NAME, '.*')
55
+ case ARGV
56
+ when []
57
+ if (readme = update_readme(readme))
58
+ File.write(README_FILE, readme)
59
+ else
60
+ abort 'Did not update worker options'
61
+ end
62
+ when %w[-n]
63
+ updated = update_readme(readme)
64
+ unless updated == readme
65
+ puts "Run #{program_name} to update work options in readme"
66
+ IO.popen('diff -u README.markdown -', 'w'){ |f| f << updated }
67
+ exit 1
68
+ end
56
69
  else
57
- abort 'Did not update worker options'
70
+ abort "#{program_name} [-n] (-n to change exit code instead of updating)"
58
71
  end
@@ -143,14 +143,14 @@ describe ImageOptim::BinResolver do
143
143
  expect(FSPath).to receive(:temp_dir).
144
144
  once.and_return(tmpdir)
145
145
  expect(tmpdir).to receive(:/).
146
- with(:the_optimizer).once.and_return(symlink)
146
+ with(:'the-optimizer').once.and_return(symlink)
147
147
  expect(symlink).to receive(:make_symlink).
148
148
  with(File.expand_path(path)).once
149
149
 
150
150
  expect(resolver).not_to receive(:full_path)
151
151
  bin = double
152
152
  expect(Bin).to receive(:new).
153
- with(:the_optimizer, File.expand_path(path)).and_return(bin)
153
+ with(:'the-optimizer', File.expand_path(path)).and_return(bin)
154
154
  expect(bin).to receive(:check!).once
155
155
  expect(bin).to receive(:check_fail!).exactly(5).times
156
156
 
@@ -160,7 +160,7 @@ describe ImageOptim::BinResolver do
160
160
  end
161
161
 
162
162
  5.times do
163
- resolver.resolve!(:the_optimizer)
163
+ resolver.resolve!(:'the-optimizer')
164
164
  end
165
165
  expect(resolver.env_path).to eq([
166
166
  tmpdir,
@@ -15,7 +15,9 @@ describe ImageOptim::Cache do
15
15
 
16
16
  let(:cache_dir) do
17
17
  dir = '/somewhere/cache'
18
- allow(FileUtils).to receive(:mkpath).with(Regexp.new(Regexp.escape(dir)), any_args)
18
+ allow(Dir).to receive(:mkdir).with(File.dirname(dir))
19
+ allow(Dir).to receive(:mkdir).with(dir)
20
+ allow(Dir).to receive(:mkdir).with(%r{\A#{Regexp.escape(dir)}/[^/]+\z})
19
21
  allow(FileUtils).to receive(:touch)
20
22
  allow(FSPath).to receive(:temp_file_path) do
21
23
  tmp_file
@@ -71,9 +71,7 @@ describe ImageOptim::OptionDefinition do
71
71
 
72
72
  context 'when proc given' do
73
73
  subject do
74
- # not using &:inspect due to ruby Bug #13087
75
- # to_s is just to calm rubocop
76
- described_class.new('abc', :def, 'desc'){ |o| o.inspect.to_s }
74
+ described_class.new('abc', :def, 'desc', &:inspect)
77
75
  end
78
76
 
79
77
  context 'when option not provided' do
@@ -101,7 +101,16 @@ describe ImageOptim::Runner::OptionParser do
101
101
  allow(parser).to receive(:terminal_columns).and_return(80)
102
102
 
103
103
  expect(parser.help.split("\n")).
104
- to all(satisfy{ |line| line.length <= 80 })
104
+ to all(satisfy{ |line| line.length < 80 })
105
+ end
106
+
107
+ it 'wraps texts even for too wide terminals' do
108
+ parser = OptionParser.new({})
109
+
110
+ allow(parser).to receive(:terminal_columns).and_return(1000)
111
+
112
+ expect(parser.help.split("\n")).
113
+ to all(satisfy{ |line| line.length <= 120 })
105
114
  end
106
115
  end
107
116
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'image_optim/true_false_nil'
5
+
6
+ describe ImageOptim::TrueFalseNil do
7
+ describe '.convert' do
8
+ it 'keeps true' do
9
+ expect(described_class.convert(true)).to eq(true)
10
+ end
11
+
12
+ it 'keeps false' do
13
+ expect(described_class.convert(false)).to eq(false)
14
+ end
15
+
16
+ it 'keeps nil' do
17
+ expect(described_class.convert(nil)).to eq(nil)
18
+ end
19
+
20
+ it 'converts truthy to true' do
21
+ expect(described_class.convert(1)).to eq(true)
22
+ end
23
+ end
24
+ end