deadfinder 1.7.0 → 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: 04f339906ad4505079153aea6164c57c48ef0d70540002cd49e9f56adddde355
4
- data.tar.gz: 2483fc250e5de54e3cd4349c6a91cf305372c4acf7297fb286eb7a9140678d81
3
+ metadata.gz: 5c4f184329cd4314f65259f1f4b316c50ad3953bd52eeb81fbac45907eca25ab
4
+ data.tar.gz: d6e046f05ca1088d3a9a593fce99cc52dc582687e9982b68b1b604d3e1573918
5
5
  SHA512:
6
- metadata.gz: 0f2577176f086c4ad6406b1c60e5e82a4e711e83902fd4088424d993e67de5d0a17fd9fea6e40cd8a8808a5944738393d0fd8ea7d109f19ba939db13155fc9c9
7
- data.tar.gz: beb4467d411b85e42b569a552321da6e9138fab877b4ebb2b1f8b852cd2e30b99e57a8bca8e12a5774fd83679c5a50bfdf3c3cdc21f66d716a38dd4e8833fdc7
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.0;)', 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.0'
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
@@ -40,6 +49,7 @@ module DeadFinder
40
49
  def self.run_url(url, options)
41
50
  DeadFinder::Logger.apply_options(options)
42
51
  run_with_target(url, options)
52
+ gen_output(options)
43
53
  end
44
54
 
45
55
  def self.run_sitemap(sitemap_url, options)
@@ -47,8 +57,10 @@ module DeadFinder
47
57
  app = Runner.new
48
58
  base_uri = URI(sitemap_url)
49
59
  sitemap = SitemapParser.new(sitemap_url, recurse: true)
50
- DeadFinder::Logger.info "Found #{sitemap.to_a.size} URLs from #{sitemap_url}"
51
- 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|
52
64
  turl = generate_url(url, base_uri)
53
65
  run_with_target(turl, options, app)
54
66
  end
@@ -59,7 +71,9 @@ module DeadFinder
59
71
  DeadFinder::Logger.apply_options(options)
60
72
  DeadFinder::Logger.info 'Reading input'
61
73
  app = Runner.new
62
- Array(yield).each do |target|
74
+ targets = Array(yield)
75
+ targets = targets.first(options['limit']) if options['limit'].positive?
76
+ targets.each do |target|
63
77
  run_with_target(target, options, app)
64
78
  end
65
79
  gen_output(options)
@@ -70,30 +84,82 @@ module DeadFinder
70
84
  app.run(target, options)
71
85
  end
72
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
+
73
119
  def self.gen_output(options)
74
120
  return if options['output'].empty?
75
121
 
76
122
  output_data = DeadFinder.output.to_h
77
123
  format = options['output_format'].to_s.downcase
78
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
+
79
128
  content = case format
80
129
  when 'yaml', 'yml'
81
- 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
82
132
  when 'csv'
83
- generate_csv(output_data)
133
+ generate_csv(output_data, coverage_info)
84
134
  else
85
- 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)
86
137
  end
87
138
 
88
139
  File.write(options['output'], content)
89
140
  end
90
141
 
91
- def self.generate_csv(output_data)
142
+ def self.generate_csv(output_data, coverage_info = nil)
92
143
  CSV.generate do |csv|
93
144
  csv << %w[target url]
94
145
  output_data.each do |target, urls|
95
146
  Array(urls).each { |url| csv << [target, url] }
96
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
97
163
  end
98
164
  end
99
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.0
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-12 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
@@ -53,42 +53,42 @@ dependencies:
53
53
  name: json
54
54
  requirement: !ruby/object:Gem::Requirement
55
55
  requirements:
56
- - - "~>"
57
- - !ruby/object:Gem::Version
58
- version: 2.6.0
59
56
  - - ">="
60
57
  - !ruby/object:Gem::Version
61
- version: 2.6.0
58
+ version: '2.6'
59
+ - - "<"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.11'
62
62
  type: :runtime
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: 2.6.0
69
66
  - - ">="
70
67
  - !ruby/object:Gem::Version
71
- version: 2.6.0
68
+ version: '2.6'
69
+ - - "<"
70
+ - !ruby/object:Gem::Version
71
+ version: '2.11'
72
72
  - !ruby/object:Gem::Dependency
73
73
  name: nokogiri
74
74
  requirement: !ruby/object:Gem::Requirement
75
75
  requirements:
76
- - - "~>"
77
- - !ruby/object:Gem::Version
78
- version: 1.13.0
79
76
  - - ">="
80
77
  - !ruby/object:Gem::Version
81
- version: 1.13.0
78
+ version: '1.13'
79
+ - - "<"
80
+ - !ruby/object:Gem::Version
81
+ version: '1.19'
82
82
  type: :runtime
83
83
  prerelease: false
84
84
  version_requirements: !ruby/object:Gem::Requirement
85
85
  requirements:
86
- - - "~>"
87
- - !ruby/object:Gem::Version
88
- version: 1.13.0
89
86
  - - ">="
90
87
  - !ruby/object:Gem::Version
91
- version: 1.13.0
88
+ version: '1.13'
89
+ - - "<"
90
+ - !ruby/object:Gem::Version
91
+ version: '1.19'
92
92
  - !ruby/object:Gem::Dependency
93
93
  name: open-uri
94
94
  requirement: !ruby/object:Gem::Requirement
@@ -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