jekyll-minifier 0.1.10 → 0.2.1
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 +4 -4
- data/.dockerignore +8 -0
- data/.github/FUNDING.yml +3 -0
- data/CLAUDE.md +96 -0
- data/COVERAGE_ANALYSIS.md +228 -0
- data/Dockerfile +30 -0
- data/FINAL_TEST_REPORT.md +164 -0
- data/README.md +17 -12
- data/SECURITY.md +155 -0
- data/SECURITY_FIX_SUMMARY.md +141 -0
- data/VALIDATION_FEATURES.md +254 -0
- data/cody-mcp.db +0 -0
- data/docker-compose.yml +42 -0
- data/example_config.yml +127 -0
- data/issue48-basic/_config.yml +7 -0
- data/issue48-basic/_layouts/default.html +23 -0
- data/issue48-basic/assets/css/style.css +10 -0
- data/issue48-basic/assets/js/script.js +9 -0
- data/issue48-basic/index.html +5 -0
- data/jekyll-minifier.gemspec +9 -9
- data/lib/jekyll-minifier/version.rb +1 -1
- data/lib/jekyll-minifier.rb +1169 -126
- data/spec/caching_performance_spec.rb +238 -0
- data/spec/compressor_cache_spec.rb +326 -0
- data/spec/coverage_enhancement_spec.rb +391 -0
- data/spec/enhanced_css_spec.rb +277 -0
- data/spec/environment_validation_spec.rb +84 -0
- data/spec/fixtures/_config.yml +2 -2
- data/spec/fixtures/assets/data.json +25 -0
- data/spec/fixtures/assets/js/script.js +21 -0
- data/spec/input_validation_spec.rb +514 -0
- data/spec/jekyll-minifier_enhanced_spec.rb +211 -0
- data/spec/jekyll-minifier_spec.rb +61 -0
- data/spec/performance_spec.rb +232 -0
- data/spec/security_redos_spec.rb +306 -0
- data/spec/security_validation_spec.rb +253 -0
- metadata +73 -19
@@ -0,0 +1,238 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'benchmark'
|
3
|
+
|
4
|
+
describe "Jekyll::Minifier Caching Performance" do
|
5
|
+
let(:config) { Jekyll::Minifier::CompressionConfig.new({}) }
|
6
|
+
let(:factory) { Jekyll::Minifier::CompressorFactory }
|
7
|
+
let(:cache) { Jekyll::Minifier::CompressorCache }
|
8
|
+
|
9
|
+
before(:each) do
|
10
|
+
cache.clear_all
|
11
|
+
end
|
12
|
+
|
13
|
+
after(:all) do
|
14
|
+
Jekyll::Minifier::CompressorCache.clear_all
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "compressor creation performance" do
|
18
|
+
it "demonstrates significant performance improvement with caching" do
|
19
|
+
iterations = 50
|
20
|
+
|
21
|
+
# Benchmark without caching (clear cache each time)
|
22
|
+
time_without_caching = Benchmark.realtime do
|
23
|
+
iterations.times do
|
24
|
+
cache.clear_all
|
25
|
+
factory.create_css_compressor(config)
|
26
|
+
factory.create_js_compressor(config)
|
27
|
+
factory.create_html_compressor(config)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Benchmark with caching
|
32
|
+
cache.clear_all
|
33
|
+
time_with_caching = Benchmark.realtime do
|
34
|
+
iterations.times do
|
35
|
+
factory.create_css_compressor(config)
|
36
|
+
factory.create_js_compressor(config)
|
37
|
+
factory.create_html_compressor(config)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
puts "\nCaching Performance Results:"
|
42
|
+
puts "Without caching: #{(time_without_caching * 1000).round(2)}ms (#{(time_without_caching * 1000 / iterations).round(2)}ms per iteration)"
|
43
|
+
puts "With caching: #{(time_with_caching * 1000).round(2)}ms (#{(time_with_caching * 1000 / iterations).round(2)}ms per iteration)"
|
44
|
+
|
45
|
+
improvement_ratio = time_without_caching / time_with_caching
|
46
|
+
puts "Performance improvement: #{improvement_ratio.round(2)}x faster"
|
47
|
+
|
48
|
+
# Cache should show high hit ratio
|
49
|
+
stats = cache.stats
|
50
|
+
puts "Cache hit ratio: #{(cache.hit_ratio * 100).round(1)}%"
|
51
|
+
puts "Cache statistics: #{stats}"
|
52
|
+
|
53
|
+
# Verify significant performance improvement
|
54
|
+
expect(improvement_ratio).to be > 2.0, "Caching should provide at least 2x performance improvement"
|
55
|
+
expect(cache.hit_ratio).to be > 0.8, "Cache hit ratio should be above 80%"
|
56
|
+
end
|
57
|
+
|
58
|
+
it "shows memory efficiency with reasonable cache size" do
|
59
|
+
# Create many different configurations
|
60
|
+
20.times do |i|
|
61
|
+
test_config = Jekyll::Minifier::CompressionConfig.new({
|
62
|
+
'jekyll-minifier' => {
|
63
|
+
'terser_args' => { 'compress' => (i % 2 == 0), 'mangle' => (i % 3 == 0) }
|
64
|
+
}
|
65
|
+
})
|
66
|
+
|
67
|
+
factory.create_css_compressor(test_config)
|
68
|
+
factory.create_js_compressor(test_config)
|
69
|
+
factory.create_html_compressor(test_config)
|
70
|
+
end
|
71
|
+
|
72
|
+
sizes = cache.cache_sizes
|
73
|
+
puts "\nMemory Efficiency Results:"
|
74
|
+
puts "Cache sizes: #{sizes}"
|
75
|
+
|
76
|
+
# Verify cache size limits are respected
|
77
|
+
expect(sizes[:css]).to be <= Jekyll::Minifier::CompressorCache::MAX_CACHE_SIZE
|
78
|
+
expect(sizes[:js]).to be <= Jekyll::Minifier::CompressorCache::MAX_CACHE_SIZE
|
79
|
+
expect(sizes[:html]).to be <= Jekyll::Minifier::CompressorCache::MAX_CACHE_SIZE
|
80
|
+
expect(sizes[:total]).to be <= Jekyll::Minifier::CompressorCache::MAX_CACHE_SIZE * 3
|
81
|
+
end
|
82
|
+
|
83
|
+
it "demonstrates compression performance with cached compressors" do
|
84
|
+
css_content = "body { color: red; background-color: blue; margin: 10px; padding: 5px; }"
|
85
|
+
js_content = "function test() { var message = 'hello world'; console.log(message); return message; }"
|
86
|
+
html_content = "<html><head><title>Test</title></head><body><h1>Test</h1><p>Content</p></body></html>"
|
87
|
+
|
88
|
+
iterations = 30
|
89
|
+
|
90
|
+
# Benchmark compression performance with fresh compressors
|
91
|
+
cache.clear_all
|
92
|
+
time_without_cache = Benchmark.realtime do
|
93
|
+
iterations.times do
|
94
|
+
cache.clear_all
|
95
|
+
factory.compress_css(css_content, config, "test.css")
|
96
|
+
factory.compress_js(js_content, config, "test.js")
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Benchmark compression performance with cached compressors
|
101
|
+
cache.clear_all
|
102
|
+
time_with_cache = Benchmark.realtime do
|
103
|
+
iterations.times do
|
104
|
+
factory.compress_css(css_content, config, "test.css")
|
105
|
+
factory.compress_js(js_content, config, "test.js")
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
puts "\nCompression Performance Results:"
|
110
|
+
puts "Without cache: #{(time_without_cache * 1000).round(2)}ms"
|
111
|
+
puts "With cache: #{(time_with_cache * 1000).round(2)}ms"
|
112
|
+
|
113
|
+
improvement_ratio = time_without_cache / time_with_cache
|
114
|
+
puts "Compression improvement: #{improvement_ratio.round(2)}x faster"
|
115
|
+
|
116
|
+
# Verify compression performance improvement
|
117
|
+
expect(improvement_ratio).to be > 1.5, "Caching should improve compression performance by at least 50%"
|
118
|
+
end
|
119
|
+
|
120
|
+
it "maintains thread safety under concurrent load" do
|
121
|
+
threads = []
|
122
|
+
errors = []
|
123
|
+
iterations_per_thread = 10
|
124
|
+
thread_count = 5
|
125
|
+
|
126
|
+
cache.clear_all
|
127
|
+
|
128
|
+
# Create multiple threads performing compression
|
129
|
+
thread_count.times do |t|
|
130
|
+
threads << Thread.new do
|
131
|
+
begin
|
132
|
+
iterations_per_thread.times do |i|
|
133
|
+
config_data = {
|
134
|
+
'jekyll-minifier' => {
|
135
|
+
'terser_args' => { 'compress' => ((t + i) % 2 == 0) }
|
136
|
+
}
|
137
|
+
}
|
138
|
+
test_config = Jekyll::Minifier::CompressionConfig.new(config_data)
|
139
|
+
|
140
|
+
compressor = factory.create_js_compressor(test_config)
|
141
|
+
result = compressor.compile("function test() { return true; }")
|
142
|
+
|
143
|
+
Thread.current[:results] = (Thread.current[:results] || []) << result
|
144
|
+
end
|
145
|
+
rescue => e
|
146
|
+
errors << e
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
# Wait for completion
|
152
|
+
threads.each(&:join)
|
153
|
+
|
154
|
+
# Verify no errors occurred
|
155
|
+
expect(errors).to be_empty, "No thread safety errors should occur: #{errors.inspect}"
|
156
|
+
|
157
|
+
# Verify all threads got results
|
158
|
+
total_results = threads.sum { |t| (t[:results] || []).length }
|
159
|
+
expect(total_results).to eq(thread_count * iterations_per_thread)
|
160
|
+
|
161
|
+
puts "\nThread Safety Results:"
|
162
|
+
puts "Threads: #{thread_count}, Iterations per thread: #{iterations_per_thread}"
|
163
|
+
puts "Total operations: #{total_results}"
|
164
|
+
puts "Errors: #{errors.length}"
|
165
|
+
puts "Final cache stats: #{cache.stats}"
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
describe "cache behavior validation" do
|
170
|
+
it "properly limits cache size and demonstrates eviction capability" do
|
171
|
+
max_size = Jekyll::Minifier::CompressorCache::MAX_CACHE_SIZE
|
172
|
+
|
173
|
+
# Test cache size limiting by creating configurations we know will be different
|
174
|
+
# Use direct cache interface to verify behavior
|
175
|
+
test_objects = []
|
176
|
+
(1..(max_size + 3)).each do |i|
|
177
|
+
cache_key = "test_key_#{i}"
|
178
|
+
obj = cache.get_or_create(:css, cache_key) { "test_object_#{i}" }
|
179
|
+
test_objects << obj
|
180
|
+
end
|
181
|
+
|
182
|
+
puts "\nDirect Cache Test Results:"
|
183
|
+
puts "Created #{test_objects.length} objects"
|
184
|
+
puts "Cache sizes: #{cache.cache_sizes}"
|
185
|
+
puts "Cache stats: #{cache.stats}"
|
186
|
+
|
187
|
+
# Verify cache respects size limits
|
188
|
+
expect(cache.cache_sizes[:css]).to eq(max_size)
|
189
|
+
expect(cache.stats[:evictions]).to be > 0
|
190
|
+
expect(test_objects.length).to eq(max_size + 3)
|
191
|
+
|
192
|
+
# Test that early entries were evicted
|
193
|
+
first_key_result = cache.get_or_create(:css, "test_key_1") { "recreated_object_1" }
|
194
|
+
expect(first_key_result).to eq("recreated_object_1") # Should be recreated, not cached
|
195
|
+
|
196
|
+
puts "LRU Eviction confirmed: first entry was evicted and recreated"
|
197
|
+
end
|
198
|
+
|
199
|
+
it "correctly identifies cache hits vs misses" do
|
200
|
+
config1 = Jekyll::Minifier::CompressionConfig.new({
|
201
|
+
'jekyll-minifier' => { 'terser_args' => { 'compress' => true } }
|
202
|
+
})
|
203
|
+
config2 = Jekyll::Minifier::CompressionConfig.new({
|
204
|
+
'jekyll-minifier' => { 'terser_args' => { 'compress' => false } }
|
205
|
+
})
|
206
|
+
|
207
|
+
cache.clear_all
|
208
|
+
|
209
|
+
# First access - should be miss
|
210
|
+
factory.create_js_compressor(config1)
|
211
|
+
stats1 = cache.stats
|
212
|
+
|
213
|
+
# Second access same config - should be hit
|
214
|
+
factory.create_js_compressor(config1)
|
215
|
+
stats2 = cache.stats
|
216
|
+
|
217
|
+
# Third access different config - should be miss
|
218
|
+
factory.create_js_compressor(config2)
|
219
|
+
stats3 = cache.stats
|
220
|
+
|
221
|
+
# Fourth access first config - should be hit
|
222
|
+
factory.create_js_compressor(config1)
|
223
|
+
stats4 = cache.stats
|
224
|
+
|
225
|
+
puts "\nCache Hit/Miss Tracking:"
|
226
|
+
puts "After 1st call (config1): hits=#{stats1[:hits]}, misses=#{stats1[:misses]}"
|
227
|
+
puts "After 2nd call (config1): hits=#{stats2[:hits]}, misses=#{stats2[:misses]}"
|
228
|
+
puts "After 3rd call (config2): hits=#{stats3[:hits]}, misses=#{stats3[:misses]}"
|
229
|
+
puts "After 4th call (config1): hits=#{stats4[:hits]}, misses=#{stats4[:misses]}"
|
230
|
+
|
231
|
+
expect(stats1[:misses]).to eq(1)
|
232
|
+
expect(stats1[:hits]).to eq(0)
|
233
|
+
expect(stats2[:hits]).to eq(1)
|
234
|
+
expect(stats3[:misses]).to eq(2)
|
235
|
+
expect(stats4[:hits]).to eq(2)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
@@ -0,0 +1,326 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "Jekyll::Minifier::CompressorCache" do
|
4
|
+
let(:cache) { Jekyll::Minifier::CompressorCache }
|
5
|
+
|
6
|
+
before(:each) do
|
7
|
+
# Clear cache before each test
|
8
|
+
cache.clear_all
|
9
|
+
end
|
10
|
+
|
11
|
+
after(:all) do
|
12
|
+
# Clean up after all tests
|
13
|
+
Jekyll::Minifier::CompressorCache.clear_all
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "cache key generation" do
|
17
|
+
it "generates consistent keys for identical configurations" do
|
18
|
+
config1 = { terser_args: { compress: true, mangle: false } }
|
19
|
+
config2 = { terser_args: { compress: true, mangle: false } }
|
20
|
+
|
21
|
+
key1 = cache.generate_cache_key(config1)
|
22
|
+
key2 = cache.generate_cache_key(config2)
|
23
|
+
|
24
|
+
expect(key1).to eq(key2)
|
25
|
+
expect(key1).to be_a(String)
|
26
|
+
expect(key1.length).to eq(17) # SHA256 truncated to 16 chars + null terminator handling
|
27
|
+
end
|
28
|
+
|
29
|
+
it "generates different keys for different configurations" do
|
30
|
+
config1 = { terser_args: { compress: true, mangle: false } }
|
31
|
+
config2 = { terser_args: { compress: false, mangle: true } }
|
32
|
+
|
33
|
+
key1 = cache.generate_cache_key(config1)
|
34
|
+
key2 = cache.generate_cache_key(config2)
|
35
|
+
|
36
|
+
expect(key1).not_to eq(key2)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "handles nil and empty configurations" do
|
40
|
+
expect(cache.generate_cache_key(nil)).to eq('default')
|
41
|
+
expect(cache.generate_cache_key({})).to eq('default')
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "caching functionality" do
|
46
|
+
it "caches and retrieves compressor objects" do
|
47
|
+
call_count = 0
|
48
|
+
|
49
|
+
# First call should create new object
|
50
|
+
obj1 = cache.get_or_create(:js, "test_key") do
|
51
|
+
call_count += 1
|
52
|
+
"mock_compressor_#{call_count}"
|
53
|
+
end
|
54
|
+
|
55
|
+
# Second call should retrieve cached object
|
56
|
+
obj2 = cache.get_or_create(:js, "test_key") do
|
57
|
+
call_count += 1
|
58
|
+
"mock_compressor_#{call_count}"
|
59
|
+
end
|
60
|
+
|
61
|
+
expect(obj1).to eq(obj2)
|
62
|
+
expect(call_count).to eq(1) # Factory block called only once
|
63
|
+
expect(obj1).to eq("mock_compressor_1")
|
64
|
+
end
|
65
|
+
|
66
|
+
it "maintains separate caches for different types" do
|
67
|
+
css_obj = cache.get_or_create(:css, "key1") { "css_compressor" }
|
68
|
+
js_obj = cache.get_or_create(:js, "key1") { "js_compressor" }
|
69
|
+
html_obj = cache.get_or_create(:html, "key1") { "html_compressor" }
|
70
|
+
|
71
|
+
expect(css_obj).to eq("css_compressor")
|
72
|
+
expect(js_obj).to eq("js_compressor")
|
73
|
+
expect(html_obj).to eq("html_compressor")
|
74
|
+
|
75
|
+
# Each should be independent
|
76
|
+
expect(css_obj).not_to eq(js_obj)
|
77
|
+
expect(js_obj).not_to eq(html_obj)
|
78
|
+
end
|
79
|
+
|
80
|
+
it "implements LRU eviction when cache is full" do
|
81
|
+
# Fill cache to capacity
|
82
|
+
(1..Jekyll::Minifier::CompressorCache::MAX_CACHE_SIZE).each do |i|
|
83
|
+
cache.get_or_create(:js, "key_#{i}") { "compressor_#{i}" }
|
84
|
+
end
|
85
|
+
|
86
|
+
expect(cache.cache_sizes[:js]).to eq(Jekyll::Minifier::CompressorCache::MAX_CACHE_SIZE)
|
87
|
+
|
88
|
+
# Add one more - should evict oldest
|
89
|
+
cache.get_or_create(:js, "new_key") { "new_compressor" }
|
90
|
+
|
91
|
+
expect(cache.cache_sizes[:js]).to eq(Jekyll::Minifier::CompressorCache::MAX_CACHE_SIZE)
|
92
|
+
expect(cache.stats[:evictions]).to eq(1)
|
93
|
+
|
94
|
+
# First key should be evicted
|
95
|
+
call_count = 0
|
96
|
+
cache.get_or_create(:js, "key_1") do
|
97
|
+
call_count += 1
|
98
|
+
"recreated_compressor"
|
99
|
+
end
|
100
|
+
|
101
|
+
expect(call_count).to eq(1) # Had to recreate
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe "statistics tracking" do
|
106
|
+
it "tracks cache hits and misses" do
|
107
|
+
initial_stats = cache.stats
|
108
|
+
expect(initial_stats[:hits]).to eq(0)
|
109
|
+
expect(initial_stats[:misses]).to eq(0)
|
110
|
+
|
111
|
+
# First access - should be miss
|
112
|
+
cache.get_or_create(:css, "test") { "compressor" }
|
113
|
+
stats_after_miss = cache.stats
|
114
|
+
expect(stats_after_miss[:misses]).to eq(1)
|
115
|
+
expect(stats_after_miss[:hits]).to eq(0)
|
116
|
+
|
117
|
+
# Second access - should be hit
|
118
|
+
cache.get_or_create(:css, "test") { "compressor" }
|
119
|
+
stats_after_hit = cache.stats
|
120
|
+
expect(stats_after_hit[:misses]).to eq(1)
|
121
|
+
expect(stats_after_hit[:hits]).to eq(1)
|
122
|
+
end
|
123
|
+
|
124
|
+
it "calculates hit ratio correctly" do
|
125
|
+
expect(cache.hit_ratio).to eq(0.0) # No operations yet
|
126
|
+
|
127
|
+
# One miss
|
128
|
+
cache.get_or_create(:css, "test1") { "comp1" }
|
129
|
+
expect(cache.hit_ratio).to eq(0.0)
|
130
|
+
|
131
|
+
# One hit
|
132
|
+
cache.get_or_create(:css, "test1") { "comp1" }
|
133
|
+
expect(cache.hit_ratio).to eq(0.5)
|
134
|
+
|
135
|
+
# Another hit
|
136
|
+
cache.get_or_create(:css, "test1") { "comp1" }
|
137
|
+
expect(cache.hit_ratio).to be_within(0.01).of(0.67)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
describe "thread safety" do
|
142
|
+
it "handles concurrent access safely" do
|
143
|
+
threads = []
|
144
|
+
results = {}
|
145
|
+
|
146
|
+
# Create multiple threads accessing cache concurrently
|
147
|
+
10.times do |i|
|
148
|
+
threads << Thread.new do
|
149
|
+
key = "concurrent_key_#{i % 3}" # Use some duplicate keys
|
150
|
+
result = cache.get_or_create(:js, key) { "compressor_#{key}" }
|
151
|
+
Thread.current[:result] = result
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Wait for all threads to complete
|
156
|
+
threads.each(&:join)
|
157
|
+
|
158
|
+
# Collect results
|
159
|
+
threads.each_with_index do |thread, i|
|
160
|
+
results[i] = thread[:result]
|
161
|
+
end
|
162
|
+
|
163
|
+
# Verify no race conditions occurred
|
164
|
+
expect(results.values.uniq.length).to eq(3) # Should have 3 unique compressors
|
165
|
+
expect(cache.cache_sizes[:js]).to eq(3)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
describe "memory management" do
|
170
|
+
it "limits cache size appropriately" do
|
171
|
+
# Add more than max cache size
|
172
|
+
(1..(Jekyll::Minifier::CompressorCache::MAX_CACHE_SIZE + 5)).each do |i|
|
173
|
+
cache.get_or_create(:css, "key_#{i}") { "compressor_#{i}" }
|
174
|
+
end
|
175
|
+
|
176
|
+
sizes = cache.cache_sizes
|
177
|
+
expect(sizes[:css]).to eq(Jekyll::Minifier::CompressorCache::MAX_CACHE_SIZE)
|
178
|
+
expect(sizes[:js]).to eq(0)
|
179
|
+
expect(sizes[:html]).to eq(0)
|
180
|
+
expect(sizes[:total]).to eq(Jekyll::Minifier::CompressorCache::MAX_CACHE_SIZE)
|
181
|
+
end
|
182
|
+
|
183
|
+
it "clears all caches completely" do
|
184
|
+
# Add some data to each cache
|
185
|
+
cache.get_or_create(:css, "css_key") { "css_comp" }
|
186
|
+
cache.get_or_create(:js, "js_key") { "js_comp" }
|
187
|
+
cache.get_or_create(:html, "html_key") { "html_comp" }
|
188
|
+
|
189
|
+
expect(cache.cache_sizes[:total]).to eq(3)
|
190
|
+
|
191
|
+
cache.clear_all
|
192
|
+
|
193
|
+
expect(cache.cache_sizes[:total]).to eq(0)
|
194
|
+
expect(cache.stats[:hits]).to eq(0)
|
195
|
+
expect(cache.stats[:misses]).to eq(0)
|
196
|
+
expect(cache.stats[:evictions]).to eq(0)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
describe "Jekyll::Minifier::CompressorFactory with Caching" do
|
202
|
+
let(:config) { Jekyll::Minifier::CompressionConfig.new({}) }
|
203
|
+
let(:factory) { Jekyll::Minifier::CompressorFactory }
|
204
|
+
let(:cache) { Jekyll::Minifier::CompressorCache }
|
205
|
+
|
206
|
+
before(:each) do
|
207
|
+
cache.clear_all
|
208
|
+
end
|
209
|
+
|
210
|
+
after(:all) do
|
211
|
+
Jekyll::Minifier::CompressorCache.clear_all
|
212
|
+
end
|
213
|
+
|
214
|
+
describe "CSS compressor caching" do
|
215
|
+
it "caches CSS compressors based on configuration" do
|
216
|
+
initial_stats = cache.stats
|
217
|
+
|
218
|
+
# First call should create new compressor
|
219
|
+
comp1 = factory.create_css_compressor(config)
|
220
|
+
stats_after_first = cache.stats
|
221
|
+
expect(stats_after_first[:misses]).to eq(initial_stats[:misses] + 1)
|
222
|
+
|
223
|
+
# Second call with same config should return cached compressor
|
224
|
+
comp2 = factory.create_css_compressor(config)
|
225
|
+
stats_after_second = cache.stats
|
226
|
+
expect(stats_after_second[:hits]).to eq(initial_stats[:hits] + 1)
|
227
|
+
|
228
|
+
# Should be the same object
|
229
|
+
expect(comp1).to be(comp2)
|
230
|
+
end
|
231
|
+
|
232
|
+
it "creates different compressors for different configurations" do
|
233
|
+
config1 = Jekyll::Minifier::CompressionConfig.new({
|
234
|
+
'jekyll-minifier' => { 'css_enhanced_mode' => false }
|
235
|
+
})
|
236
|
+
config2 = Jekyll::Minifier::CompressionConfig.new({
|
237
|
+
'jekyll-minifier' => {
|
238
|
+
'css_enhanced_mode' => true,
|
239
|
+
'css_merge_duplicate_selectors' => true
|
240
|
+
}
|
241
|
+
})
|
242
|
+
|
243
|
+
comp1 = factory.create_css_compressor(config1)
|
244
|
+
comp2 = factory.create_css_compressor(config2)
|
245
|
+
|
246
|
+
# Should be different objects for different configurations
|
247
|
+
expect(comp1).not_to be(comp2)
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
describe "JavaScript compressor caching" do
|
252
|
+
it "caches JS compressors based on Terser configuration" do
|
253
|
+
initial_stats = cache.stats
|
254
|
+
|
255
|
+
comp1 = factory.create_js_compressor(config)
|
256
|
+
stats_after_first = cache.stats
|
257
|
+
expect(stats_after_first[:misses]).to be > initial_stats[:misses]
|
258
|
+
|
259
|
+
comp2 = factory.create_js_compressor(config)
|
260
|
+
stats_after_second = cache.stats
|
261
|
+
expect(stats_after_second[:hits]).to be > initial_stats[:hits]
|
262
|
+
|
263
|
+
expect(comp1).to be(comp2)
|
264
|
+
end
|
265
|
+
|
266
|
+
it "creates different compressors for different Terser configurations" do
|
267
|
+
config1 = Jekyll::Minifier::CompressionConfig.new({})
|
268
|
+
config2 = Jekyll::Minifier::CompressionConfig.new({
|
269
|
+
'jekyll-minifier' => {
|
270
|
+
'terser_args' => { 'compress' => false, 'mangle' => false }
|
271
|
+
}
|
272
|
+
})
|
273
|
+
|
274
|
+
comp1 = factory.create_js_compressor(config1)
|
275
|
+
comp2 = factory.create_js_compressor(config2)
|
276
|
+
|
277
|
+
expect(comp1).not_to be(comp2)
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
describe "HTML compressor caching" do
|
282
|
+
it "caches HTML compressors based on full configuration" do
|
283
|
+
initial_stats = cache.stats
|
284
|
+
|
285
|
+
comp1 = factory.create_html_compressor(config)
|
286
|
+
comp2 = factory.create_html_compressor(config)
|
287
|
+
|
288
|
+
final_stats = cache.stats
|
289
|
+
expect(final_stats[:hits]).to be > initial_stats[:hits]
|
290
|
+
expect(comp1).to be(comp2)
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
describe "integration with compression methods" do
|
295
|
+
it "benefits from caching in CSS compression" do
|
296
|
+
css_content = "body { color: red; background-color: blue; }"
|
297
|
+
|
298
|
+
cache.clear_all
|
299
|
+
initial_stats = cache.stats
|
300
|
+
|
301
|
+
# First compression
|
302
|
+
result1 = factory.compress_css(css_content, config, "test1.css")
|
303
|
+
|
304
|
+
# Second compression
|
305
|
+
result2 = factory.compress_css(css_content, config, "test2.css")
|
306
|
+
|
307
|
+
final_stats = cache.stats
|
308
|
+
expect(final_stats[:hits]).to be > initial_stats[:hits]
|
309
|
+
expect(result1).to eq(result2) # Same compression result
|
310
|
+
end
|
311
|
+
|
312
|
+
it "benefits from caching in JS compression" do
|
313
|
+
js_content = "function test() { return 'hello world'; }"
|
314
|
+
|
315
|
+
cache.clear_all
|
316
|
+
initial_stats = cache.stats
|
317
|
+
|
318
|
+
result1 = factory.compress_js(js_content, config, "test1.js")
|
319
|
+
result2 = factory.compress_js(js_content, config, "test2.js")
|
320
|
+
|
321
|
+
final_stats = cache.stats
|
322
|
+
expect(final_stats[:hits]).to be > initial_stats[:hits]
|
323
|
+
expect(result1).to eq(result2)
|
324
|
+
end
|
325
|
+
end
|
326
|
+
end
|