crawlscope 0.3.0 → 0.5.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 +4 -4
- data/CHANGELOG.md +73 -0
- data/README.md +50 -7
- data/lib/crawlscope/cli.rb +4 -1
- data/lib/crawlscope/crawl.rb +2 -0
- data/lib/crawlscope/document_text.rb +40 -0
- data/lib/crawlscope/rake_tasks.rb +27 -12
- data/lib/crawlscope/reporter.rb +20 -5
- data/lib/crawlscope/rule_registry.rb +3 -1
- data/lib/crawlscope/rules/content_quality.rb +99 -0
- data/lib/crawlscope/rules/indexability.rb +179 -0
- data/lib/crawlscope/rules/links.rb +312 -9
- data/lib/crawlscope/rules/metadata.rb +61 -6
- data/lib/crawlscope/rules/structured_data.rb +31 -0
- data/lib/crawlscope/rules/uniqueness.rb +98 -4
- data/lib/crawlscope/sitemap.rb +9 -1
- data/lib/crawlscope/version.rb +1 -1
- data/lib/tasks/crawlscope_tasks.rake +28 -18
- data/test/crawlscope/cli_test.rb +1 -0
- data/test/crawlscope/configuration_test.rb +8 -1
- data/test/crawlscope/content_quality_rule_test.rb +68 -0
- data/test/crawlscope/crawl_test.rb +37 -1
- data/test/crawlscope/indexability_rule_test.rb +129 -0
- data/test/crawlscope/links_rule_test.rb +148 -3
- data/test/crawlscope/metadata_rule_test.rb +36 -0
- data/test/crawlscope/rake_tasks_test.rb +70 -0
- data/test/crawlscope/reporter_test.rb +7 -3
- data/test/crawlscope/sitemap_test.rb +24 -0
- data/test/crawlscope/structured_data_rule_test.rb +56 -0
- data/test/crawlscope/uniqueness_rule_test.rb +59 -3
- data/test/release_task_test.rb +86 -0
- metadata +9 -2
|
@@ -23,7 +23,7 @@ class CrawlscopeReporterTest < Minitest::Test
|
|
|
23
23
|
refute_includes output, "Status: FAILED"
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
def
|
|
26
|
+
def test_reports_failed_result_with_grouped_counts_and_offenses
|
|
27
27
|
io = StringIO.new
|
|
28
28
|
issues = Crawlscope::IssueCollection.new
|
|
29
29
|
issues.add(code: :missing_title, severity: :warning, category: :metadata, url: "https://example.com/a", message: "missing <title>", details: {})
|
|
@@ -42,9 +42,13 @@ class CrawlscopeReporterTest < Minitest::Test
|
|
|
42
42
|
|
|
43
43
|
assert_includes output, "Status: FAILED"
|
|
44
44
|
assert_includes output, "Issues: 2"
|
|
45
|
+
assert_includes output, "Severity:"
|
|
45
46
|
assert_includes output, "notice: 1"
|
|
46
47
|
assert_includes output, "warning: 1"
|
|
47
|
-
assert_includes output, "
|
|
48
|
-
assert_includes output, "
|
|
48
|
+
assert_includes output, "Category:"
|
|
49
|
+
assert_includes output, "links: 1"
|
|
50
|
+
assert_includes output, "metadata: 1"
|
|
51
|
+
assert_includes output, " - [warning] missing_title https://example.com/a missing <title>"
|
|
52
|
+
assert_includes output, " - [notice] broken_internal_link https://example.com/b broken internal link"
|
|
49
53
|
end
|
|
50
54
|
end
|
|
@@ -49,6 +49,30 @@ class CrawlscopeSitemapTest < Minitest::Test
|
|
|
49
49
|
assert_equal ["https://www.example.com/features/reviews"], parser.urls(base_url: "https://www.example.com")
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
+
def test_remote_sitemap_http_error_is_explicit
|
|
53
|
+
stub_request(:get, "https://www.example.com/sitemap.xml")
|
|
54
|
+
.to_return(status: 500, body: "<html><body>Error</body></html>")
|
|
55
|
+
|
|
56
|
+
parser = Crawlscope::Sitemap.new(path: "https://www.example.com/sitemap.xml")
|
|
57
|
+
|
|
58
|
+
error = assert_raises(Crawlscope::ValidationError) do
|
|
59
|
+
parser.urls(base_url: "https://www.example.com")
|
|
60
|
+
end
|
|
61
|
+
assert_equal "Sitemap https://www.example.com/sitemap.xml returned HTTP 500", error.message
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def test_invalid_sitemap_root_is_explicit
|
|
65
|
+
stub_request(:get, "https://www.example.com/sitemap.xml")
|
|
66
|
+
.to_return(status: 200, body: "<html><body>Error</body></html>")
|
|
67
|
+
|
|
68
|
+
parser = Crawlscope::Sitemap.new(path: "https://www.example.com/sitemap.xml")
|
|
69
|
+
|
|
70
|
+
error = assert_raises(Crawlscope::ValidationError) do
|
|
71
|
+
parser.urls(base_url: "https://www.example.com")
|
|
72
|
+
end
|
|
73
|
+
assert_equal 'Sitemap https://www.example.com/sitemap.xml has unexpected root "html"', error.message
|
|
74
|
+
end
|
|
75
|
+
|
|
52
76
|
def test_rebases_remote_sitemap_index_children_to_base_url
|
|
53
77
|
stub_request(:get, "http://localhost:3000/sitemap.xml")
|
|
54
78
|
.to_return(
|
|
@@ -79,6 +79,62 @@ class CrawlscopeStructuredDataRuleTest < Minitest::Test
|
|
|
79
79
|
assert_equal ["json-ld", "microdata"], issues.to_a.first.details[:expected_sources]
|
|
80
80
|
end
|
|
81
81
|
|
|
82
|
+
def test_reports_structured_data_missing_type
|
|
83
|
+
issues = Crawlscope::IssueCollection.new
|
|
84
|
+
rule = Crawlscope::Rules::StructuredData.new
|
|
85
|
+
page = page(
|
|
86
|
+
url: "https://example.com/articles/test",
|
|
87
|
+
body: <<~HTML
|
|
88
|
+
<html>
|
|
89
|
+
<head>
|
|
90
|
+
<script type="application/ld+json">
|
|
91
|
+
{"@context":"https://schema.org","headline":"Untyped article"}
|
|
92
|
+
</script>
|
|
93
|
+
</head>
|
|
94
|
+
<body><h1>Article</h1></body>
|
|
95
|
+
</html>
|
|
96
|
+
HTML
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
rule.call(
|
|
100
|
+
urls: [page.url],
|
|
101
|
+
pages: [page],
|
|
102
|
+
issues: issues,
|
|
103
|
+
context: {schema_registry: Crawlscope::SchemaRegistry.default}
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
assert_includes issues.to_a.map(&:code), :structured_data_missing_type
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def test_reports_graph_entries_missing_type
|
|
110
|
+
issues = Crawlscope::IssueCollection.new
|
|
111
|
+
rule = Crawlscope::Rules::StructuredData.new
|
|
112
|
+
page = page(
|
|
113
|
+
url: "https://example.com/articles/test",
|
|
114
|
+
body: <<~HTML
|
|
115
|
+
<html>
|
|
116
|
+
<head>
|
|
117
|
+
<script type="application/ld+json">
|
|
118
|
+
{"@context":"https://schema.org","@type":"WebPage","@graph":[{"name":"Untyped node"}]}
|
|
119
|
+
</script>
|
|
120
|
+
</head>
|
|
121
|
+
<body><h1>Article</h1></body>
|
|
122
|
+
</html>
|
|
123
|
+
HTML
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
rule.call(
|
|
127
|
+
urls: [page.url],
|
|
128
|
+
pages: [page],
|
|
129
|
+
issues: issues,
|
|
130
|
+
context: {schema_registry: Crawlscope::SchemaRegistry.default}
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
issue = issues.to_a.find { |item| item.code == :structured_data_missing_type }
|
|
134
|
+
assert issue
|
|
135
|
+
assert_equal ["$.@graph[0]"], issue.details[:paths]
|
|
136
|
+
end
|
|
137
|
+
|
|
82
138
|
def test_validates_job_posting_markup
|
|
83
139
|
issues = Crawlscope::IssueCollection.new
|
|
84
140
|
rule = Crawlscope::Rules::StructuredData.new
|
|
@@ -13,18 +13,74 @@ class CrawlscopeUniquenessRuleTest < Minitest::Test
|
|
|
13
13
|
|
|
14
14
|
rule.call(urls: pages.map(&:url), pages: pages, issues: issues, context: {})
|
|
15
15
|
|
|
16
|
-
assert_equal %i[duplicate_content_fingerprint duplicate_meta_description duplicate_title].sort, issues.to_a.map(&:code).sort
|
|
16
|
+
assert_equal %i[duplicate_content_fingerprint duplicate_meta_description duplicate_pages_without_canonical duplicate_title].sort, issues.to_a.map(&:code).sort
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def test_allows_duplicate_pages_when_canonicals_are_present
|
|
20
|
+
issues = Crawlscope::IssueCollection.new
|
|
21
|
+
rule = Crawlscope::Rules::Uniqueness.new
|
|
22
|
+
pages = [
|
|
23
|
+
page(url: "https://example.com/a", canonical: "https://example.com/a"),
|
|
24
|
+
page(url: "https://example.com/b", canonical: "https://example.com/a")
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
rule.call(urls: pages.map(&:url), pages: pages, issues: issues, context: {})
|
|
28
|
+
|
|
29
|
+
refute_includes issues.to_a.map(&:code), :duplicate_pages_without_canonical
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_reports_near_duplicate_content
|
|
33
|
+
issues = Crawlscope::IssueCollection.new
|
|
34
|
+
rule = Crawlscope::Rules::Uniqueness.new
|
|
35
|
+
pages = [
|
|
36
|
+
page(url: "https://example.com/a", content: near_duplicate_content("reliable")),
|
|
37
|
+
page(url: "https://example.com/b", content: near_duplicate_content("dependable"))
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
rule.call(urls: pages.map(&:url), pages: pages, issues: issues, context: {})
|
|
41
|
+
|
|
42
|
+
issue = issues.to_a.find { |item| item.code == :near_duplicate_content }
|
|
43
|
+
assert issue
|
|
44
|
+
assert_operator issue.details[:similarity], :>=, issue.details[:threshold]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_skips_near_duplicate_scan_when_page_count_exceeds_limit
|
|
48
|
+
issues = Crawlscope::IssueCollection.new
|
|
49
|
+
rule = Crawlscope::Rules::Uniqueness.new(max_near_duplicate_pages: 1)
|
|
50
|
+
pages = [
|
|
51
|
+
page(url: "https://example.com/a", content: near_duplicate_content("reliable")),
|
|
52
|
+
page(url: "https://example.com/b", content: near_duplicate_content("dependable"))
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
rule.call(urls: pages.map(&:url), pages: pages, issues: issues, context: {})
|
|
56
|
+
|
|
57
|
+
skip_issue = issues.to_a.find { |item| item.code == :near_duplicate_scan_skipped }
|
|
58
|
+
refute issues.to_a.any? { |item| item.code == :near_duplicate_content }
|
|
59
|
+
assert_equal :warning, skip_issue.severity
|
|
60
|
+
assert_equal({max_pages: 1, page_count: 2}, skip_issue.details)
|
|
17
61
|
end
|
|
18
62
|
|
|
19
63
|
private
|
|
20
64
|
|
|
21
|
-
def
|
|
22
|
-
|
|
65
|
+
def near_duplicate_content(adjective)
|
|
66
|
+
<<~TEXT.gsub(/\s+/, " ").strip
|
|
67
|
+
This page summarizes practical hotel review patterns for operators who need #{adjective}
|
|
68
|
+
service insights across locations. It compares recurring comments about staff, rooms,
|
|
69
|
+
cleanliness, check-in, breakfast, parking, and amenities so teams can prioritize fixes.
|
|
70
|
+
The analysis highlights repeat themes, explains why guests mention them, and keeps the
|
|
71
|
+
wording focused on decisions that improve daily operations.
|
|
72
|
+
TEXT
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def page(url:, content: nil, canonical: nil)
|
|
76
|
+
repeated_text = content || ("Useful content " * 30).strip
|
|
77
|
+
canonical_tag = canonical ? %(<link rel="canonical" href="#{canonical}">) : ""
|
|
23
78
|
body = <<~HTML
|
|
24
79
|
<html>
|
|
25
80
|
<head>
|
|
26
81
|
<title>Example Title</title>
|
|
27
82
|
<meta name="description" content="Example description">
|
|
83
|
+
#{canonical_tag}
|
|
28
84
|
</head>
|
|
29
85
|
<body>
|
|
30
86
|
<main>#{repeated_text}</main>
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
require "rake"
|
|
5
|
+
|
|
6
|
+
unless respond_to?(:release_version, true)
|
|
7
|
+
load File.expand_path("../Rakefile", __dir__)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class ReleaseTaskTest < Minitest::Test
|
|
11
|
+
def test_release_version_increments_patch_from_current_version
|
|
12
|
+
major, minor, patch = Crawlscope::VERSION.split(".").map(&:to_i)
|
|
13
|
+
|
|
14
|
+
assert_equal "#{major}.#{minor}.#{patch + 1}", release_version("patch")
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def test_release_version_accepts_explicit_semantic_version
|
|
18
|
+
assert_equal "0.3.0", release_version("0.3.0")
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def test_validate_release_version_rejects_current_version
|
|
22
|
+
error = assert_raises(ArgumentError) do
|
|
23
|
+
validate_release_version!("0.2.7", "0.2.7")
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
assert_equal(
|
|
27
|
+
"Release version 0.2.7 must be newer than current version 0.2.7.",
|
|
28
|
+
error.message
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def test_validate_release_version_rejects_existing_local_tag
|
|
33
|
+
@local_release_tag_exists = true
|
|
34
|
+
@remote_release_tag_exists = false
|
|
35
|
+
|
|
36
|
+
error = assert_raises(ArgumentError) do
|
|
37
|
+
validate_release_version!("0.2.8", "0.2.7")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
assert_equal "Release tag v0.2.8 already exists locally.", error.message
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def test_validate_release_version_rejects_existing_remote_tag
|
|
44
|
+
@local_release_tag_exists = false
|
|
45
|
+
@remote_release_tag_exists = true
|
|
46
|
+
|
|
47
|
+
error = assert_raises(ArgumentError) do
|
|
48
|
+
validate_release_version!("0.2.8", "0.2.7")
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
assert_equal "Release tag v0.2.8 already exists on origin.", error.message
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_remote_release_tag_command_asks_git_to_fail_when_no_tag_matches
|
|
55
|
+
assert_equal(
|
|
56
|
+
"git ls-remote --exit-code --tags origin refs/tags/v0.2.8",
|
|
57
|
+
remote_release_tag_command("v0.2.8")
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def test_changelog_command_prepends_the_next_release
|
|
62
|
+
assert_equal(
|
|
63
|
+
[
|
|
64
|
+
"git-cliff",
|
|
65
|
+
"-c",
|
|
66
|
+
"cliff.toml",
|
|
67
|
+
"--unreleased",
|
|
68
|
+
"--tag",
|
|
69
|
+
"v0.2.8",
|
|
70
|
+
"--prepend",
|
|
71
|
+
"CHANGELOG.md"
|
|
72
|
+
],
|
|
73
|
+
changelog_command("0.2.8")
|
|
74
|
+
)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def local_release_tag_exists?(_tag)
|
|
80
|
+
@local_release_tag_exists || false
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def remote_release_tag_exists?(_tag)
|
|
84
|
+
@remote_release_tag_exists || false
|
|
85
|
+
end
|
|
86
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: crawlscope
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Paulo Fidalgo
|
|
@@ -199,6 +199,7 @@ files:
|
|
|
199
199
|
- lib/crawlscope/context.rb
|
|
200
200
|
- lib/crawlscope/crawl.rb
|
|
201
201
|
- lib/crawlscope/crawler.rb
|
|
202
|
+
- lib/crawlscope/document_text.rb
|
|
202
203
|
- lib/crawlscope/http.rb
|
|
203
204
|
- lib/crawlscope/issue.rb
|
|
204
205
|
- lib/crawlscope/issue_collection.rb
|
|
@@ -208,6 +209,8 @@ files:
|
|
|
208
209
|
- lib/crawlscope/reporter.rb
|
|
209
210
|
- lib/crawlscope/result.rb
|
|
210
211
|
- lib/crawlscope/rule_registry.rb
|
|
212
|
+
- lib/crawlscope/rules/content_quality.rb
|
|
213
|
+
- lib/crawlscope/rules/indexability.rb
|
|
211
214
|
- lib/crawlscope/rules/links.rb
|
|
212
215
|
- lib/crawlscope/rules/metadata.rb
|
|
213
216
|
- lib/crawlscope/rules/structured_data.rb
|
|
@@ -228,12 +231,15 @@ files:
|
|
|
228
231
|
- test/crawlscope/browser_test.rb
|
|
229
232
|
- test/crawlscope/cli_test.rb
|
|
230
233
|
- test/crawlscope/configuration_test.rb
|
|
234
|
+
- test/crawlscope/content_quality_rule_test.rb
|
|
231
235
|
- test/crawlscope/crawl_test.rb
|
|
232
236
|
- test/crawlscope/crawler_test.rb
|
|
233
237
|
- test/crawlscope/http_test.rb
|
|
238
|
+
- test/crawlscope/indexability_rule_test.rb
|
|
234
239
|
- test/crawlscope/links_rule_test.rb
|
|
235
240
|
- test/crawlscope/loader_test.rb
|
|
236
241
|
- test/crawlscope/metadata_rule_test.rb
|
|
242
|
+
- test/crawlscope/rake_tasks_test.rb
|
|
237
243
|
- test/crawlscope/reporter_test.rb
|
|
238
244
|
- test/crawlscope/rule_registry_test.rb
|
|
239
245
|
- test/crawlscope/run_test.rb
|
|
@@ -247,6 +253,7 @@ files:
|
|
|
247
253
|
- test/crawlscope/structured_data_writer_test.rb
|
|
248
254
|
- test/crawlscope/uniqueness_rule_test.rb
|
|
249
255
|
- test/crawlscope/url_test.rb
|
|
256
|
+
- test/release_task_test.rb
|
|
250
257
|
- test/test_helper.rb
|
|
251
258
|
homepage: https://www.ethos-link.com/opensource/crawlscope
|
|
252
259
|
licenses:
|
|
@@ -275,7 +282,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
275
282
|
- !ruby/object:Gem::Version
|
|
276
283
|
version: '0'
|
|
277
284
|
requirements: []
|
|
278
|
-
rubygems_version: 4.0.
|
|
285
|
+
rubygems_version: 4.0.10
|
|
279
286
|
specification_version: 4
|
|
280
287
|
summary: Audit sitemap URLs for metadata, structured data, uniqueness, and links
|
|
281
288
|
test_files: []
|