cronv-rb 0.2.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 +7 -0
- data/.github/workflows/ai-fix.yml +61 -0
- data/.github/workflows/ai-review.yml +63 -0
- data/.github/workflows/ai-triage.yml +61 -0
- data/.github/workflows/auto-merge.yml +82 -0
- data/.github/workflows/codeql.yml +29 -0
- data/.github/workflows/compare_cronv_outputs.yml +67 -0
- data/.github/workflows/release-please.yml +132 -0
- data/.github/workflows/rspec.yml +32 -0
- data/.github/workflows/rubocop.yml +32 -0
- data/.gitignore +15 -0
- data/.release-please-manifest.json +3 -0
- data/.rspec +3 -0
- data/.rubocop.yml +11 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +46 -0
- data/Gemfile +14 -0
- data/LICENSE +21 -0
- data/README.md +72 -0
- data/Rakefile +8 -0
- data/bin/console +18 -0
- data/bin/setup +8 -0
- data/cronv-rb.gemspec +44 -0
- data/docs/release.md +136 -0
- data/exe/cronv +8 -0
- data/fixtures/sample.crontab +80 -0
- data/lib/cronv_rb/crontab.rb +98 -0
- data/lib/cronv_rb/logger.rb +25 -0
- data/lib/cronv_rb/main.rb +34 -0
- data/lib/cronv_rb/option.rb +61 -0
- data/lib/cronv_rb/record.rb +51 -0
- data/lib/cronv_rb/schedule.rb +25 -0
- data/lib/cronv_rb/template.rb +152 -0
- data/lib/cronv_rb/version.rb +5 -0
- data/lib/cronv_rb/visualizer.rb +54 -0
- data/lib/cronv_rb.rb +12 -0
- data/release-please-config.json +20 -0
- data/renovate.json +43 -0
- data/scripts/compare_cronv_outputs.rb +140 -0
- metadata +125 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module CronvRb
|
|
4
|
+
class Schedule
|
|
5
|
+
attr_reader :minutes, :hour, :day_of_month, :month, :day_of_week, :year, :schedule_alias
|
|
6
|
+
|
|
7
|
+
# rubocop:disable Metrics/ParameterLists
|
|
8
|
+
def initialize(minutes:, hour:, day_of_month:, month:, day_of_week:, year:, schedule_alias:)
|
|
9
|
+
@minutes = minutes
|
|
10
|
+
@hour = hour
|
|
11
|
+
@day_of_month = day_of_month
|
|
12
|
+
@month = month
|
|
13
|
+
@day_of_week = day_of_week
|
|
14
|
+
@year = year
|
|
15
|
+
@schedule_alias = schedule_alias
|
|
16
|
+
end
|
|
17
|
+
# rubocop:enable Metrics/ParameterLists
|
|
18
|
+
|
|
19
|
+
def to_crontab
|
|
20
|
+
return @schedule_alias if @schedule_alias
|
|
21
|
+
|
|
22
|
+
"#{@minutes} #{@hour} #{@day_of_month} #{@month} #{@day_of_week} #{@year}"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'erb'
|
|
4
|
+
require 'time'
|
|
5
|
+
|
|
6
|
+
module CronvRb
|
|
7
|
+
# rubocop:disable Metrics/ClassLength
|
|
8
|
+
class Template
|
|
9
|
+
TEMPLATE = <<~ERB
|
|
10
|
+
<html>
|
|
11
|
+
<head>
|
|
12
|
+
<title><%= @v.option.title %> | <%= date_format(value: @v.time_from, format: "%Y/%-m/%-d %H:%M") %>, +<%= @v.option.duration %></title>
|
|
13
|
+
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<div class="container-fluid">
|
|
17
|
+
<h1>
|
|
18
|
+
<%= @v.option.title %> <small class="text-muted">From <%= date_format(value: @v.time_from, format: "%Y/%-m/%-d %H:%M") %>, +<%= @v.option.duration %></small>
|
|
19
|
+
</h1>
|
|
20
|
+
|
|
21
|
+
<br>
|
|
22
|
+
|
|
23
|
+
<% if @v.extras.any? %>
|
|
24
|
+
<h3>Extra</h3>
|
|
25
|
+
<div id="cronv-extra" style="width:<%= @v.option.width %>%;">
|
|
26
|
+
<dl class="row">
|
|
27
|
+
<% @v.extras.each do |extra| %>
|
|
28
|
+
<dt class="col-sm-1"><%= extra.label %></dt>
|
|
29
|
+
<dd class="col-sm-11"><%= extra.job %></dd>
|
|
30
|
+
<% end %>
|
|
31
|
+
</dl>
|
|
32
|
+
</div>
|
|
33
|
+
<hr>
|
|
34
|
+
<% end %>
|
|
35
|
+
|
|
36
|
+
<h3>Timeline</h3>
|
|
37
|
+
<div id="cronv-timeline" style="height:100%; width:<%= @v.option.width %>%;">
|
|
38
|
+
<b>Loading...</b>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>
|
|
42
|
+
<script type="text/javascript">
|
|
43
|
+
google.charts.load("current", {packages:["timeline"]});
|
|
44
|
+
google.charts.setOnLoadCallback(function() {
|
|
45
|
+
var container = document.getElementById('cronv-timeline');
|
|
46
|
+
var chart = new google.visualization.Timeline(container);
|
|
47
|
+
var dataTable = new google.visualization.DataTable();
|
|
48
|
+
dataTable.addColumn({ type: 'string', id: 'job' });
|
|
49
|
+
dataTable.addColumn({ type: 'string', id: 'dummy bar label' });
|
|
50
|
+
dataTable.addColumn({ type: 'string', role: 'tooltip' });
|
|
51
|
+
dataTable.addColumn({ type: 'date', id: 'Start' });
|
|
52
|
+
dataTable.addColumn({ type: 'date', id: 'End' });
|
|
53
|
+
|
|
54
|
+
var tasks = {};
|
|
55
|
+
<%# time_from = @v.time_from %>
|
|
56
|
+
<%# time_to = @v.time_to %>
|
|
57
|
+
<% @v.records.each do |cronv| %>
|
|
58
|
+
<% job = js_escape_string(str: cronv.crontab.job) %>
|
|
59
|
+
tasks['<%= job %>'] = tasks['<%= job %>'] || [];
|
|
60
|
+
<% if running_every_minutes?(crontab: cronv.crontab) %>
|
|
61
|
+
tasks['<%= job %>'].push(['<%= job %>', '', 'Every minutes <%= job %>', <%= new_js_date(value: @v.time_from) %>, <%= new_js_date(value: @v.time_to) %>]);
|
|
62
|
+
<% else %>
|
|
63
|
+
<% cronv_iter(cronv:).each do |exec| %>tasks['<%= job %>'].push(['<%= job %>', '', '<%= date_format(value: exec[:start], format: "%H:%M") %> <%= job %>', <%= new_js_date(value: exec[:start]) %>, <%= new_js_date(value: exec[:end]) %>]);<% end %>
|
|
64
|
+
<% end %>
|
|
65
|
+
<% end %>
|
|
66
|
+
|
|
67
|
+
var taskByJobCount = [];
|
|
68
|
+
for (var k in tasks) taskByJobCount.push({name: k, size: tasks[k].length});
|
|
69
|
+
taskByJobCount.sort(function(a, b) {
|
|
70
|
+
if (a.size == b.size) return 0;
|
|
71
|
+
return a.size > b.size ? -1 : 1;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
var rows = [];
|
|
75
|
+
for (var i = 0; i < taskByJobCount.length; i++) {
|
|
76
|
+
jobs = tasks[taskByJobCount[i].name];
|
|
77
|
+
var jl = jobs.length;
|
|
78
|
+
for (var j = 0; j < jl; j++) rows.push(jobs[j]);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (rows.length > 0) {
|
|
82
|
+
dataTable.addRows(rows);
|
|
83
|
+
chart.draw(dataTable, {
|
|
84
|
+
timeline: {
|
|
85
|
+
colorByRowLabel: true
|
|
86
|
+
},
|
|
87
|
+
avoidOverlappingGridLines: false
|
|
88
|
+
});
|
|
89
|
+
} else {
|
|
90
|
+
container.innerHTML = '<div class="alert alert-success"><strong>Woops!</strong> There is no data!</div>';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
var mousePosX = undefined,
|
|
94
|
+
mousePosY = undefined;
|
|
95
|
+
|
|
96
|
+
google.visualization.events.addListener(chart, 'onmouseover', function(e) {
|
|
97
|
+
var t = document.getElementsByClassName("google-visualization-tooltip")[0];
|
|
98
|
+
if (mousePosX) t.style.left = mousePosX + 'px';
|
|
99
|
+
if (mousePosY) t.style.top = mousePosY - 120 + 'px';
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
document.addEventListener('mousemove', function(e) {
|
|
103
|
+
mousePosX = e.pageX;
|
|
104
|
+
mousePosY = e.pageY;
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
</script>
|
|
109
|
+
</body>
|
|
110
|
+
</html>
|
|
111
|
+
ERB
|
|
112
|
+
|
|
113
|
+
def self.render(visualizer:)
|
|
114
|
+
new(visualizer:).render
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def render
|
|
118
|
+
ERB.new(TEMPLATE).result(binding)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private_class_method :new
|
|
122
|
+
|
|
123
|
+
private
|
|
124
|
+
|
|
125
|
+
def initialize(visualizer:)
|
|
126
|
+
@v = visualizer
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def cronv_iter(cronv:)
|
|
130
|
+
cronv.iter
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def js_escape_string(str:)
|
|
134
|
+
str.gsub(/['"\\]/) { |char| "\\#{char}" }.gsub("\n", '\\n').gsub("\r", '\\r')
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def new_js_date(value:)
|
|
138
|
+
time = value.is_a?(String) ? Time.parse(value) : value
|
|
139
|
+
format('new Date(%<year>d, %<month>d, %<day>d, %<hour>d, %<min>d)', year: time.year, month: time.month - 1, day: time.day, hour: time.hour, min: time.min)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def date_format(value:, format:)
|
|
143
|
+
time = value.is_a?(String) ? Time.parse(value) : value
|
|
144
|
+
time.strftime(format)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def running_every_minutes?(crontab:)
|
|
148
|
+
crontab.running_every_minutes?(crontab.schedule)
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
# rubocop:enable Metrics/ClassLength
|
|
152
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'logger'
|
|
4
|
+
require 'time'
|
|
5
|
+
|
|
6
|
+
module CronvRb
|
|
7
|
+
class Visualizer
|
|
8
|
+
attr_reader :option, :time_from, :time_to, :duration_minutes, :records, :extras
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def new_visualizer(option)
|
|
12
|
+
time_from = option.to_from_time
|
|
13
|
+
duration_minutes = option.to_duration_minutes
|
|
14
|
+
time_to = time_from + duration_minutes * 60
|
|
15
|
+
|
|
16
|
+
new(option, time_from, time_to, duration_minutes)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def add(line)
|
|
21
|
+
trimmed = line.strip
|
|
22
|
+
|
|
23
|
+
return [false, nil] if trimmed.empty? || trimmed.start_with?('#')
|
|
24
|
+
|
|
25
|
+
begin
|
|
26
|
+
record, extra = Record.new_record(line: trimmed, start_time: @time_from, duration_minutes: @duration_minutes)
|
|
27
|
+
rescue ArgumentError => e
|
|
28
|
+
warn "[WARN] Skipping invalid crontab line: '#{trimmed}' (#{e.message})"
|
|
29
|
+
return [false, nil]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
@records.push(record) unless record.nil?
|
|
33
|
+
@extras.push(extra) unless extra.nil?
|
|
34
|
+
|
|
35
|
+
[true, nil]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def dump
|
|
39
|
+
File.write(@option.output_file_path, Template.render(visualizer: self))
|
|
40
|
+
@option.output_file_path
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def initialize(option, time_from, time_to, duration_minutes)
|
|
46
|
+
@option = option
|
|
47
|
+
@time_from = time_from
|
|
48
|
+
@time_to = time_to
|
|
49
|
+
@duration_minutes = duration_minutes
|
|
50
|
+
@records = []
|
|
51
|
+
@extras = []
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
data/lib/cronv_rb.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Dir[File.join(__dir__, 'cronv_rb', '*.rb')].each do |file|
|
|
4
|
+
require "cronv_rb/#{File.basename(file, '.rb')}"
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
ENV['TZ'] = 'UTC'
|
|
8
|
+
|
|
9
|
+
module CronvRb
|
|
10
|
+
class Error < StandardError; end
|
|
11
|
+
# Your code goes here...
|
|
12
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
|
|
3
|
+
"packages": {
|
|
4
|
+
".": {
|
|
5
|
+
"release-type": "ruby",
|
|
6
|
+
"version-file": "lib/cronv_rb/version.rb",
|
|
7
|
+
"bump-minor-pre-major": true,
|
|
8
|
+
"bump-patch-for-minor-pre-major": false,
|
|
9
|
+
"include-component-in-tag": false,
|
|
10
|
+
"changelog-sections": [
|
|
11
|
+
{ "type": "feat", "section": "Features" },
|
|
12
|
+
{ "type": "fix", "section": "Bug Fixes" },
|
|
13
|
+
{ "type": "perf", "section": "Performance Improvements" },
|
|
14
|
+
{ "type": "docs", "section": "Documentation", "hidden": true },
|
|
15
|
+
{ "type": "refactor", "section": "Code Refactoring", "hidden": true },
|
|
16
|
+
{ "type": "chore", "section": "Miscellaneous", "hidden": true }
|
|
17
|
+
]
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
data/renovate.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
3
|
+
"extends": ["config:recommended"],
|
|
4
|
+
"semanticCommits": "enabled",
|
|
5
|
+
"semanticCommitType": "fix",
|
|
6
|
+
"semanticCommitScope": "deps",
|
|
7
|
+
"labels": ["dependencies"],
|
|
8
|
+
"addLabels": ["renovate"],
|
|
9
|
+
"schedule": ["every weekend"],
|
|
10
|
+
"prConcurrentLimit": 5,
|
|
11
|
+
"platformAutomerge": false,
|
|
12
|
+
"osvVulnerabilityAlerts": true,
|
|
13
|
+
"vulnerabilityAlerts": {
|
|
14
|
+
"enabled": true,
|
|
15
|
+
"labels": ["dependencies", "security"],
|
|
16
|
+
"automerge": true,
|
|
17
|
+
"schedule": ["at any time"]
|
|
18
|
+
},
|
|
19
|
+
"packageRules": [
|
|
20
|
+
{
|
|
21
|
+
"description": "Terraform (github-repo-management) owns these distributed workflow files; Renovate must not edit them or it fights Terraform over the same content.",
|
|
22
|
+
"matchFileNames": [
|
|
23
|
+
".github/workflows/ai-triage.yml",
|
|
24
|
+
".github/workflows/ai-fix.yml",
|
|
25
|
+
".github/workflows/ai-review.yml",
|
|
26
|
+
".github/workflows/auto-merge.yml",
|
|
27
|
+
".github/workflows/codeql.yml",
|
|
28
|
+
".github/workflows/digest.yml",
|
|
29
|
+
".github/workflows/drift.yml"
|
|
30
|
+
],
|
|
31
|
+
"enabled": false
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"matchUpdateTypes": ["patch", "minor"],
|
|
35
|
+
"automerge": true
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"matchUpdateTypes": ["major"],
|
|
39
|
+
"automerge": false,
|
|
40
|
+
"labels": ["dependencies", "major-update"]
|
|
41
|
+
}
|
|
42
|
+
]
|
|
43
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Compares the HTML outputs of OSS cronv (Go) and cronv-rb (Ruby) to verify
|
|
5
|
+
# that job names and color/time ranges match.
|
|
6
|
+
#
|
|
7
|
+
# Usage:
|
|
8
|
+
# ruby scripts/compare_cronv_outputs.rb <oss_cronv.html> <cronvrb.html>
|
|
9
|
+
|
|
10
|
+
def usage
|
|
11
|
+
"Usage: #{$PROGRAM_NAME} <oss_cronv.html> <cronvrb.html>"
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
abort usage if ARGV.length != 2
|
|
15
|
+
|
|
16
|
+
oss_path, rb_path = ARGV
|
|
17
|
+
|
|
18
|
+
abort "File not found: #{oss_path}" unless File.exist?(oss_path)
|
|
19
|
+
abort "File not found: #{rb_path}" unless File.exist?(rb_path)
|
|
20
|
+
|
|
21
|
+
# Parse the "Extra" section (e.g. @reboot entries) from the HTML.
|
|
22
|
+
# Returns an array of { label:, job: } hashes.
|
|
23
|
+
def parse_extras(html)
|
|
24
|
+
extras = []
|
|
25
|
+
html.scan(%r{<dt[^>]*>(.*?)</dt>\s*<dd[^>]*>(.*?)</dd>}m) do |label, command|
|
|
26
|
+
extras << { label: label.strip, job: command.strip }
|
|
27
|
+
end
|
|
28
|
+
extras
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Normalize a new Date(...) string by removing all spaces after commas/parens
|
|
32
|
+
# so that "new Date(2025, 0, 1, 0, 5)" == "new Date(2025,0,1,0,5)".
|
|
33
|
+
def normalize_date(str)
|
|
34
|
+
str.gsub(/\s+/, '')
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Parse all job names that are initialized in the JS block (tasks['job'] = ...).
|
|
38
|
+
# Returns a Set of job name strings.
|
|
39
|
+
def parse_all_job_names(html)
|
|
40
|
+
jobs = Set.new
|
|
41
|
+
html.scan(/tasks\['(.+?)'\]\s*=\s*tasks\[/) { |m| jobs << m[0] }
|
|
42
|
+
jobs
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Parse timeline task entries from the JS block in the HTML.
|
|
46
|
+
# Returns a hash: { job_name => [{start:, end:}, ...] }
|
|
47
|
+
def parse_timeline(html)
|
|
48
|
+
timeline = Hash.new { |h, k| h[k] = [] }
|
|
49
|
+
|
|
50
|
+
html.scan(/tasks\['(.+?)'\]\.push\(\['(?:.+?)', '', '(?:.+?)', (new Date\(.+?\)), (new Date\(.+?\))\]\);/) do |job, start_date, end_date|
|
|
51
|
+
timeline[job] << { start: normalize_date(start_date), end: normalize_date(end_date) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
timeline
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
puts 'Comparing:'
|
|
58
|
+
puts " OSS cronv : #{oss_path}"
|
|
59
|
+
puts " cronv-rb : #{rb_path}"
|
|
60
|
+
puts
|
|
61
|
+
|
|
62
|
+
oss_html = File.read(oss_path)
|
|
63
|
+
rb_html = File.read(rb_path)
|
|
64
|
+
|
|
65
|
+
errors = []
|
|
66
|
+
|
|
67
|
+
# ── 1. Compare Extras (e.g. @reboot) ─────────────────────────────────────────
|
|
68
|
+
oss_extras = parse_extras(oss_html)
|
|
69
|
+
rb_extras = parse_extras(rb_html)
|
|
70
|
+
|
|
71
|
+
if oss_extras != rb_extras
|
|
72
|
+
errors << 'Extra section mismatch:'
|
|
73
|
+
errors << " OSS cronv : #{oss_extras.inspect}"
|
|
74
|
+
errors << " cronv-rb : #{rb_extras.inspect}"
|
|
75
|
+
else
|
|
76
|
+
puts "[OK] Extras match (#{oss_extras.size} entries)"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# ── 2. Compare full job name set (including jobs with no executions) ───────────
|
|
80
|
+
oss_all_jobs = parse_all_job_names(oss_html)
|
|
81
|
+
rb_all_jobs = parse_all_job_names(rb_html)
|
|
82
|
+
|
|
83
|
+
only_in_oss_all = oss_all_jobs - rb_all_jobs
|
|
84
|
+
only_in_rb_all = rb_all_jobs - oss_all_jobs
|
|
85
|
+
|
|
86
|
+
if only_in_oss_all.any?
|
|
87
|
+
errors << 'Jobs present in OSS cronv but missing from cronv-rb:'
|
|
88
|
+
only_in_oss_all.sort.each { |j| errors << " - #{j}" }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
if only_in_rb_all.any?
|
|
92
|
+
errors << 'Jobs present in cronv-rb but missing from OSS cronv:'
|
|
93
|
+
only_in_rb_all.sort.each { |j| errors << " - #{j}" }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
puts "[OK] Job names match (#{oss_all_jobs.size} jobs)" if only_in_oss_all.empty? && only_in_rb_all.empty?
|
|
97
|
+
|
|
98
|
+
# ── 3. Compare time ranges for jobs that have executions ─────────────────────
|
|
99
|
+
oss_timeline = parse_timeline(oss_html)
|
|
100
|
+
rb_timeline = parse_timeline(rb_html)
|
|
101
|
+
|
|
102
|
+
common_jobs = oss_all_jobs & rb_all_jobs
|
|
103
|
+
range_errors = []
|
|
104
|
+
|
|
105
|
+
common_jobs.sort.each do |job|
|
|
106
|
+
oss_entries = oss_timeline[job].sort_by { |e| e[:start] }
|
|
107
|
+
rb_entries = rb_timeline[job].sort_by { |e| e[:start] }
|
|
108
|
+
|
|
109
|
+
next if oss_entries == rb_entries
|
|
110
|
+
|
|
111
|
+
range_errors << " Job: #{job}"
|
|
112
|
+
range_errors << " OSS cronv count : #{oss_entries.size}"
|
|
113
|
+
range_errors << " cronv-rb count : #{rb_entries.size}"
|
|
114
|
+
|
|
115
|
+
# Show up to 3 differing entries
|
|
116
|
+
diffs = oss_entries.zip(rb_entries).reject { |a, b| a == b }.first(3)
|
|
117
|
+
diffs.each_with_index do |(oss_e, rb_e), i|
|
|
118
|
+
range_errors << " Diff ##{i + 1}:"
|
|
119
|
+
range_errors << " OSS: #{oss_e.inspect}"
|
|
120
|
+
range_errors << " RB : #{rb_e.inspect}"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
if range_errors.empty?
|
|
125
|
+
puts "[OK] Time ranges match for all #{common_jobs.size} jobs"
|
|
126
|
+
else
|
|
127
|
+
errors << 'Time range mismatches:'
|
|
128
|
+
errors.concat(range_errors)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# ── Result ────────────────────────────────────────────────────────────────────
|
|
132
|
+
puts
|
|
133
|
+
if errors.empty?
|
|
134
|
+
puts 'All checks passed. cronv-rb output matches OSS cronv.'
|
|
135
|
+
exit 0
|
|
136
|
+
else
|
|
137
|
+
puts 'FAILED: Differences found between OSS cronv and cronv-rb:'
|
|
138
|
+
errors.each { |e| puts e }
|
|
139
|
+
exit 1
|
|
140
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: cronv-rb
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.2.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Masato Yamashita
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: bundler
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.5'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.5'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rake
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '13.4'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '13.4'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rspec
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '3.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '3.0'
|
|
54
|
+
description: A gem that rewrites cronv, originally written in Go, in Ruby to visualize
|
|
55
|
+
your cron schedules in crontab.
|
|
56
|
+
email:
|
|
57
|
+
- 4653960+M-Yamashita01@users.noreply.github.com
|
|
58
|
+
executables:
|
|
59
|
+
- cronv
|
|
60
|
+
extensions: []
|
|
61
|
+
extra_rdoc_files: []
|
|
62
|
+
files:
|
|
63
|
+
- ".github/workflows/ai-fix.yml"
|
|
64
|
+
- ".github/workflows/ai-review.yml"
|
|
65
|
+
- ".github/workflows/ai-triage.yml"
|
|
66
|
+
- ".github/workflows/auto-merge.yml"
|
|
67
|
+
- ".github/workflows/codeql.yml"
|
|
68
|
+
- ".github/workflows/compare_cronv_outputs.yml"
|
|
69
|
+
- ".github/workflows/release-please.yml"
|
|
70
|
+
- ".github/workflows/rspec.yml"
|
|
71
|
+
- ".github/workflows/rubocop.yml"
|
|
72
|
+
- ".gitignore"
|
|
73
|
+
- ".release-please-manifest.json"
|
|
74
|
+
- ".rspec"
|
|
75
|
+
- ".rubocop.yml"
|
|
76
|
+
- ".ruby-version"
|
|
77
|
+
- CHANGELOG.md
|
|
78
|
+
- Gemfile
|
|
79
|
+
- LICENSE
|
|
80
|
+
- README.md
|
|
81
|
+
- Rakefile
|
|
82
|
+
- bin/console
|
|
83
|
+
- bin/setup
|
|
84
|
+
- cronv-rb.gemspec
|
|
85
|
+
- docs/release.md
|
|
86
|
+
- exe/cronv
|
|
87
|
+
- fixtures/sample.crontab
|
|
88
|
+
- lib/cronv_rb.rb
|
|
89
|
+
- lib/cronv_rb/crontab.rb
|
|
90
|
+
- lib/cronv_rb/logger.rb
|
|
91
|
+
- lib/cronv_rb/main.rb
|
|
92
|
+
- lib/cronv_rb/option.rb
|
|
93
|
+
- lib/cronv_rb/record.rb
|
|
94
|
+
- lib/cronv_rb/schedule.rb
|
|
95
|
+
- lib/cronv_rb/template.rb
|
|
96
|
+
- lib/cronv_rb/version.rb
|
|
97
|
+
- lib/cronv_rb/visualizer.rb
|
|
98
|
+
- release-please-config.json
|
|
99
|
+
- renovate.json
|
|
100
|
+
- scripts/compare_cronv_outputs.rb
|
|
101
|
+
homepage: https://github.com/M-Yamashita01/cronv-rb
|
|
102
|
+
licenses:
|
|
103
|
+
- MIT
|
|
104
|
+
metadata:
|
|
105
|
+
homepage_uri: https://github.com/M-Yamashita01/cronv-rb
|
|
106
|
+
source_code_uri: https://github.com/M-Yamashita01/cronv-rb
|
|
107
|
+
changelog_uri: https://github.com/M-Yamashita01/cronv-rb
|
|
108
|
+
rdoc_options: []
|
|
109
|
+
require_paths:
|
|
110
|
+
- lib
|
|
111
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
112
|
+
requirements:
|
|
113
|
+
- - ">="
|
|
114
|
+
- !ruby/object:Gem::Version
|
|
115
|
+
version: '3.2'
|
|
116
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
117
|
+
requirements:
|
|
118
|
+
- - ">="
|
|
119
|
+
- !ruby/object:Gem::Version
|
|
120
|
+
version: '0'
|
|
121
|
+
requirements: []
|
|
122
|
+
rubygems_version: 4.0.20
|
|
123
|
+
specification_version: 4
|
|
124
|
+
summary: Visualize your cron schedules in crontab by Ruby
|
|
125
|
+
test_files: []
|