deadfinder 1.7.1 → 1.8.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,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 98d859458bc87d32005ad05d4e3f90103aab430037b4b439951911e760eea968
4
- data.tar.gz: 347a37265add728676f196a8d313dfda37a54b48eb45030f6ff01dba15b152d6
3
+ metadata.gz: 5c4f184329cd4314f65259f1f4b316c50ad3953bd52eeb81fbac45907eca25ab
4
+ data.tar.gz: d6e046f05ca1088d3a9a593fce99cc52dc582687e9982b68b1b604d3e1573918
5
5
  SHA512:
6
- metadata.gz: dc059649e3a8cfd4e644651db38e1082b4b1060624cd2d1d6d43064c43c87c836583912c6430c00f0799ee77526917d2357286cd2129c4e6176e62457c6dc007
7
- data.tar.gz: 17d62439486da8d4f2efe8ce957744c8fca8ee23a5670fb7aa128d9b688e4b7dcf7c8689355553431a171ac14e2c2a6d58515a9ec9a2c4d4ea1f5bfc40c482f2
6
+ metadata.gz: 9c8dd4b7c1047808c8ac57228f9b1ab7ea02b948e8f42c37766a189058b7b44cb50822b4d4632faf45b27046364bdb08b0cc679c7ed2c8a95f2dacef818085aa
7
+ data.tar.gz: af74057d1c8e03e69e47253b07f685796f3978e9c5da75a33efd33ae7d8f004f147555a03c529c33bd871fd7830b887cc27c1a9e851f93d8725daa24036cefef
@@ -15,7 +15,7 @@ module DeadFinder
15
15
  class_option :headers, aliases: :H, default: [], type: :array,
16
16
  desc: 'Custom HTTP headers to send with initial request'
17
17
  class_option :worker_headers, default: [], type: :array, desc: 'Custom HTTP headers to send with worker requests'
18
- class_option :user_agent, default: 'Mozilla/5.0 (compatible; DeadFinder/1.7.1;)', type: :string,
18
+ class_option :user_agent, default: 'Mozilla/5.0 (compatible; DeadFinder/1.8.0;)', type: :string,
19
19
  desc: 'User-Agent string to use for requests'
20
20
  class_option :proxy, aliases: :p, default: '', type: :string, desc: 'Proxy server to use for requests'
21
21
  class_option :proxy_auth, default: '', type: :string, desc: 'Proxy server authentication credentials'
@@ -24,6 +24,8 @@ module DeadFinder
24
24
  class_option :silent, aliases: :s, default: false, type: :boolean, desc: 'Silent mode'
25
25
  class_option :verbose, aliases: :v, default: false, type: :boolean, desc: 'Verbose mode'
26
26
  class_option :debug, default: false, type: :boolean, desc: 'Debug mode'
27
+ class_option :limit, default: 0, type: :numeric, desc: 'Limit the number of URLs to scan'
28
+ class_option :coverage, default: false, type: :boolean, desc: 'Enable coverage tracking and reporting'
27
29
 
28
30
  def self.exit_on_failure?
29
31
  true
@@ -83,6 +83,15 @@ module DeadFinder
83
83
  jobs.close
84
84
 
85
85
  (1..jobs_size).each { ~results }
86
+
87
+ # Log coverage summary if tracking was enabled
88
+ if options['coverage'] && DeadFinder.coverage_data[target] && DeadFinder.coverage_data[target][:total] > 0
89
+ total = DeadFinder.coverage_data[target][:total]
90
+ dead = DeadFinder.coverage_data[target][:dead]
91
+ percentage = ((dead.to_f / total) * 100).round(2)
92
+ DeadFinder::Logger.sub_info "Coverage: #{dead}/#{total} URLs are dead links (#{percentage}%)"
93
+ end
94
+
86
95
  DeadFinder::Logger.sub_complete 'Task completed'
87
96
  rescue StandardError => e
88
97
  DeadFinder::Logger.error "[#{e}] #{target}"
@@ -94,6 +103,12 @@ module DeadFinder
94
103
  # Skip if already cached
95
104
  else
96
105
  CACHE_SET[j] = true
106
+ # Track total URLs tested for coverage calculation (only if coverage flag is enabled)
107
+ if options['coverage']
108
+ DeadFinder.coverage_data[target] ||= { total: 0, dead: 0 }
109
+ DeadFinder.coverage_data[target][:total] += 1
110
+ end
111
+
97
112
  begin
98
113
  CACHE_QUE[j] = true
99
114
  uri = URI.parse(j)
@@ -114,11 +129,15 @@ module DeadFinder
114
129
  CACHE_QUE[j] = false
115
130
  DeadFinder.output[target] ||= []
116
131
  DeadFinder.output[target] << j
132
+ # Track dead URLs for coverage calculation (only if coverage flag is enabled)
133
+ DeadFinder.coverage_data[target][:dead] += 1 if options['coverage']
117
134
  else
118
135
  DeadFinder::Logger.verbose_ok "[#{status_code}] #{j}" if options['verbose']
119
136
  end
120
137
  rescue StandardError => e
121
138
  DeadFinder::Logger.verbose "[#{e}] #{j}" if options['verbose']
139
+ # Consider errored URLs as dead for coverage calculation (only if coverage flag is enabled)
140
+ DeadFinder.coverage_data[target][:dead] += 1 if options['coverage']
122
141
  end
123
142
  end
124
143
  results << j
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeadFinder
4
- VERSION = '1.7.1'
4
+ VERSION = '1.8.0'
5
5
  end
data/lib/deadfinder.rb CHANGED
@@ -29,6 +29,15 @@ module DeadFinder
29
29
  @output = val
30
30
  end
31
31
 
32
+ @coverage_data = {}
33
+ def self.coverage_data
34
+ @coverage_data
35
+ end
36
+
37
+ def self.coverage_data=(val)
38
+ @coverage_data = val
39
+ end
40
+
32
41
  def self.run_pipe(options)
33
42
  run_with_input(options) { $stdin.gets&.chomp }
34
43
  end
@@ -48,8 +57,10 @@ module DeadFinder
48
57
  app = Runner.new
49
58
  base_uri = URI(sitemap_url)
50
59
  sitemap = SitemapParser.new(sitemap_url, recurse: true)
51
- DeadFinder::Logger.info "Found #{sitemap.to_a.size} URLs from #{sitemap_url}"
52
- sitemap.to_a.each do |url|
60
+ urls = sitemap.to_a
61
+ urls = urls.first(options['limit']) if options['limit'].positive?
62
+ DeadFinder::Logger.info "Found #{urls.size} URLs from #{sitemap_url}"
63
+ urls.each do |url|
53
64
  turl = generate_url(url, base_uri)
54
65
  run_with_target(turl, options, app)
55
66
  end
@@ -60,7 +71,9 @@ module DeadFinder
60
71
  DeadFinder::Logger.apply_options(options)
61
72
  DeadFinder::Logger.info 'Reading input'
62
73
  app = Runner.new
63
- Array(yield).each do |target|
74
+ targets = Array(yield)
75
+ targets = targets.first(options['limit']) if options['limit'].positive?
76
+ targets.each do |target|
64
77
  run_with_target(target, options, app)
65
78
  end
66
79
  gen_output(options)
@@ -71,30 +84,82 @@ module DeadFinder
71
84
  app.run(target, options)
72
85
  end
73
86
 
87
+ def self.calculate_coverage
88
+ coverage_summary = {}
89
+ total_all_tested = 0
90
+ total_all_dead = 0
91
+
92
+ coverage_data.each do |target, data|
93
+ total = data[:total]
94
+ dead = data[:dead]
95
+ coverage_percentage = total.positive? ? ((dead.to_f / total) * 100).round(2) : 0.0
96
+
97
+ coverage_summary[target] = {
98
+ total_tested: total,
99
+ dead_links: dead,
100
+ coverage_percentage: coverage_percentage
101
+ }
102
+
103
+ total_all_tested += total
104
+ total_all_dead += dead
105
+ end
106
+
107
+ overall_coverage = total_all_tested.positive? ? ((total_all_dead.to_f / total_all_tested) * 100).round(2) : 0.0
108
+
109
+ {
110
+ targets: coverage_summary,
111
+ summary: {
112
+ total_tested: total_all_tested,
113
+ total_dead: total_all_dead,
114
+ overall_coverage_percentage: overall_coverage
115
+ }
116
+ }
117
+ end
118
+
74
119
  def self.gen_output(options)
75
120
  return if options['output'].empty?
76
121
 
77
122
  output_data = DeadFinder.output.to_h
78
123
  format = options['output_format'].to_s.downcase
79
124
 
125
+ # Include coverage data only if coverage flag is enabled and data exists
126
+ coverage_info = calculate_coverage if options['coverage'] && coverage_data.any? && coverage_data.values.any? { |v| v[:total].positive? }
127
+
80
128
  content = case format
81
129
  when 'yaml', 'yml'
82
- output_data.to_yaml
130
+ output_with_coverage = coverage_info ? { 'dead_links' => output_data, 'coverage' => coverage_info } : output_data
131
+ output_with_coverage.to_yaml
83
132
  when 'csv'
84
- generate_csv(output_data)
133
+ generate_csv(output_data, coverage_info)
85
134
  else
86
- JSON.pretty_generate(output_data)
135
+ output_with_coverage = coverage_info ? { 'dead_links' => output_data, 'coverage' => coverage_info } : output_data
136
+ JSON.pretty_generate(output_with_coverage)
87
137
  end
88
138
 
89
139
  File.write(options['output'], content)
90
140
  end
91
141
 
92
- def self.generate_csv(output_data)
142
+ def self.generate_csv(output_data, coverage_info = nil)
93
143
  CSV.generate do |csv|
94
144
  csv << %w[target url]
95
145
  output_data.each do |target, urls|
96
146
  Array(urls).each { |url| csv << [target, url] }
97
147
  end
148
+
149
+ # Add coverage information as additional rows if available
150
+ if coverage_info
151
+ csv << [] # Empty row separator
152
+ csv << ['Coverage Report']
153
+ csv << %w[target total_tested dead_links coverage_percentage]
154
+ coverage_info[:targets].each do |target, data|
155
+ csv << [target, data[:total_tested], data[:dead_links], "#{data[:coverage_percentage]}%"]
156
+ end
157
+ csv << [] # Empty row separator
158
+ csv << ['Overall Summary']
159
+ csv << %w[total_tested total_dead overall_coverage_percentage]
160
+ summary = coverage_info[:summary]
161
+ csv << [summary[:total_tested], summary[:total_dead], "#{summary[:overall_coverage_percentage]}%"]
162
+ end
98
163
  end
99
164
  end
100
165
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: deadfinder
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.1
4
+ version: 1.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - hahwul
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-03-25 00:00:00.000000000 Z
10
+ date: 2025-08-23 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: colorize
@@ -153,22 +153,22 @@ dependencies:
153
153
  name: thor
154
154
  requirement: !ruby/object:Gem::Requirement
155
155
  requirements:
156
- - - "~>"
157
- - !ruby/object:Gem::Version
158
- version: 1.2.0
159
156
  - - ">="
160
157
  - !ruby/object:Gem::Version
161
- version: 1.2.0
158
+ version: '1.2'
159
+ - - "<"
160
+ - !ruby/object:Gem::Version
161
+ version: '1.5'
162
162
  type: :runtime
163
163
  prerelease: false
164
164
  version_requirements: !ruby/object:Gem::Requirement
165
165
  requirements:
166
- - - "~>"
167
- - !ruby/object:Gem::Version
168
- version: 1.2.0
169
166
  - - ">="
170
167
  - !ruby/object:Gem::Version
171
- version: 1.2.0
168
+ version: '1.2'
169
+ - - "<"
170
+ - !ruby/object:Gem::Version
171
+ version: '1.5'
172
172
  - !ruby/object:Gem::Dependency
173
173
  name: rspec
174
174
  requirement: !ruby/object:Gem::Requirement