wheneverd 0.3.0 → 0.4.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 +10 -0
- data/README.md +45 -2
- data/lib/wheneverd/dsl/period_parser.rb +8 -107
- data/lib/wheneverd/dsl/period_strategy/array_strategy.rb +29 -0
- data/lib/wheneverd/dsl/period_strategy/base.rb +65 -0
- data/lib/wheneverd/dsl/period_strategy/duration_strategy.rb +33 -0
- data/lib/wheneverd/dsl/period_strategy/string_strategy.rb +51 -0
- data/lib/wheneverd/dsl/period_strategy/symbol_strategy.rb +31 -0
- data/lib/wheneverd/dsl/period_strategy.rb +43 -0
- data/lib/wheneverd/duration.rb +1 -7
- data/lib/wheneverd/errors.rb +3 -0
- data/lib/wheneverd/interval.rb +22 -7
- data/lib/wheneverd/systemd/cron_parser/dow_parser.rb +208 -0
- data/lib/wheneverd/systemd/cron_parser/field_parser.rb +163 -0
- data/lib/wheneverd/systemd/cron_parser.rb +56 -303
- data/lib/wheneverd/systemd/renderer.rb +6 -64
- data/lib/wheneverd/systemd/unit_content_builder.rb +76 -0
- data/lib/wheneverd/systemd/unit_deleter.rb +2 -28
- data/lib/wheneverd/systemd/unit_lister.rb +2 -28
- data/lib/wheneverd/systemd/unit_namer.rb +6 -14
- data/lib/wheneverd/systemd/unit_path_utils.rb +54 -0
- data/lib/wheneverd/systemd/unit_writer.rb +2 -28
- data/lib/wheneverd/trigger/base.rb +22 -0
- data/lib/wheneverd/trigger/boot.rb +8 -6
- data/lib/wheneverd/trigger/calendar.rb +7 -0
- data/lib/wheneverd/trigger/interval.rb +8 -6
- data/lib/wheneverd/validation.rb +89 -0
- data/lib/wheneverd/version.rb +1 -1
- data/lib/wheneverd.rb +4 -1
- data/test/domain_model_test.rb +105 -0
- data/test/systemd_cron_parser_test.rb +41 -25
- data/test/systemd_renderer_errors_test.rb +1 -1
- metadata +13 -1
|
@@ -37,26 +37,18 @@ module Wheneverd
|
|
|
37
37
|
private_class_method :signature
|
|
38
38
|
|
|
39
39
|
def self.trigger_signature(trigger)
|
|
40
|
-
|
|
41
|
-
when Wheneverd::Trigger::Interval
|
|
42
|
-
"interval:#{trigger.seconds}"
|
|
43
|
-
when Wheneverd::Trigger::Boot
|
|
44
|
-
"boot:#{trigger.seconds}"
|
|
45
|
-
when Wheneverd::Trigger::Calendar
|
|
46
|
-
"calendar:#{trigger.on_calendar.sort.join('|')}"
|
|
47
|
-
else
|
|
40
|
+
unless trigger.respond_to?(:signature)
|
|
48
41
|
raise ArgumentError, "Unsupported trigger type: #{trigger.class}"
|
|
49
42
|
end
|
|
43
|
+
|
|
44
|
+
trigger.signature
|
|
50
45
|
end
|
|
51
46
|
private_class_method :trigger_signature
|
|
52
47
|
|
|
53
48
|
def self.job_signature(job)
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
else
|
|
58
|
-
raise ArgumentError, "Unsupported job type: #{job.class}"
|
|
59
|
-
end
|
|
49
|
+
raise ArgumentError, "Unsupported job type: #{job.class}" unless job.respond_to?(:signature)
|
|
50
|
+
|
|
51
|
+
job.signature
|
|
60
52
|
end
|
|
61
53
|
private_class_method :job_signature
|
|
62
54
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wheneverd
|
|
4
|
+
module Systemd
|
|
5
|
+
# Shared utilities for unit file path operations.
|
|
6
|
+
#
|
|
7
|
+
# These methods are used by {UnitWriter}, {UnitDeleter}, {UnitLister}, and {Renderer}
|
|
8
|
+
# to ensure consistent identifier sanitization, path pattern matching, and marker detection.
|
|
9
|
+
module UnitPathUtils
|
|
10
|
+
# Sanitizes an identifier for use in unit file names.
|
|
11
|
+
#
|
|
12
|
+
# Invalid characters are replaced with `-`, and consecutive/leading/trailing dashes
|
|
13
|
+
# are collapsed or removed.
|
|
14
|
+
#
|
|
15
|
+
# @param identifier [String]
|
|
16
|
+
# @return [String]
|
|
17
|
+
# @raise [InvalidIdentifierError] if the identifier is empty or contains no alphanumeric chars
|
|
18
|
+
def self.sanitize_identifier(identifier)
|
|
19
|
+
raw = identifier.to_s.strip
|
|
20
|
+
raise InvalidIdentifierError, "identifier must not be empty" if raw.empty?
|
|
21
|
+
|
|
22
|
+
sanitized = raw.gsub(/[^A-Za-z0-9_-]/, "-").gsub(/-+/, "-").gsub(/\A-|-+\z/, "")
|
|
23
|
+
if sanitized.empty?
|
|
24
|
+
raise InvalidIdentifierError,
|
|
25
|
+
"identifier must include at least one alphanumeric character"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
sanitized
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Returns a regex pattern matching unit basenames for the given identifier.
|
|
32
|
+
#
|
|
33
|
+
# Matches both new-style (SHA-based) and legacy (eN-jN) unit names.
|
|
34
|
+
#
|
|
35
|
+
# @param identifier [String]
|
|
36
|
+
# @return [Regexp]
|
|
37
|
+
def self.basename_pattern(identifier)
|
|
38
|
+
id = sanitize_identifier(identifier)
|
|
39
|
+
/\Awheneverd-#{Regexp.escape(id)}-(?:[0-9a-f]{12}(?:-\d+)?|e\d+-j\d+)\.(service|timer)\z/
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Checks if a unit file was generated by wheneverd.
|
|
43
|
+
#
|
|
44
|
+
# Reads the first line of the file and checks for the marker prefix.
|
|
45
|
+
#
|
|
46
|
+
# @param path [String] path to the unit file
|
|
47
|
+
# @return [Boolean]
|
|
48
|
+
def self.generated_marker?(path)
|
|
49
|
+
first_line = File.open(path, "r") { |f| f.gets.to_s }
|
|
50
|
+
first_line.start_with?(Wheneverd::Systemd::Renderer::MARKER_PREFIX)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -67,7 +67,7 @@ module Wheneverd
|
|
|
67
67
|
private_class_method :prune_stale_units
|
|
68
68
|
|
|
69
69
|
def self.stale_unit_paths(dest_dir, identifier:, keep:)
|
|
70
|
-
pattern = basename_pattern(identifier)
|
|
70
|
+
pattern = UnitPathUtils.basename_pattern(identifier)
|
|
71
71
|
Dir.children(dest_dir).filter_map do |basename|
|
|
72
72
|
next if keep.key?(basename)
|
|
73
73
|
|
|
@@ -83,36 +83,10 @@ module Wheneverd
|
|
|
83
83
|
return false unless pattern.match?(basename)
|
|
84
84
|
return false unless File.file?(path)
|
|
85
85
|
|
|
86
|
-
generated_marker?(path)
|
|
86
|
+
UnitPathUtils.generated_marker?(path)
|
|
87
87
|
end
|
|
88
88
|
private_class_method :stale_unit_path?
|
|
89
89
|
|
|
90
|
-
def self.basename_pattern(identifier)
|
|
91
|
-
id = sanitize_identifier(identifier)
|
|
92
|
-
/\Awheneverd-#{Regexp.escape(id)}-(?:[0-9a-f]{12}(?:-\d+)?|e\d+-j\d+)\.(service|timer)\z/
|
|
93
|
-
end
|
|
94
|
-
private_class_method :basename_pattern
|
|
95
|
-
|
|
96
|
-
def self.generated_marker?(path)
|
|
97
|
-
first_line = File.open(path, "r") { |f| f.gets.to_s }
|
|
98
|
-
first_line.start_with?(Wheneverd::Systemd::Renderer::MARKER_PREFIX)
|
|
99
|
-
end
|
|
100
|
-
private_class_method :generated_marker?
|
|
101
|
-
|
|
102
|
-
def self.sanitize_identifier(identifier)
|
|
103
|
-
raw = identifier.to_s.strip
|
|
104
|
-
raise InvalidIdentifierError, "identifier must not be empty" if raw.empty?
|
|
105
|
-
|
|
106
|
-
sanitized = raw.gsub(/[^A-Za-z0-9_-]/, "-").gsub(/-+/, "-").gsub(/\A-|-+\z/, "")
|
|
107
|
-
if sanitized.empty?
|
|
108
|
-
raise InvalidIdentifierError,
|
|
109
|
-
"identifier must include at least one alphanumeric character"
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
sanitized
|
|
113
|
-
end
|
|
114
|
-
private_class_method :sanitize_identifier
|
|
115
|
-
|
|
116
90
|
def self.atomic_write(dest_path, contents, dir:)
|
|
117
91
|
basename = File.basename(dest_path)
|
|
118
92
|
tmp = Tempfile.new([".#{basename}.", ".tmp"], dir)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wheneverd
|
|
4
|
+
module Trigger
|
|
5
|
+
# Base module for trigger types.
|
|
6
|
+
#
|
|
7
|
+
# All trigger types must implement:
|
|
8
|
+
# - `#systemd_timer_lines` - returns Array<String> of systemd [Timer] lines
|
|
9
|
+
# - `#signature` - returns a String signature for stable unit naming
|
|
10
|
+
module Base
|
|
11
|
+
# @return [Array<String>] systemd `[Timer]` lines for this trigger
|
|
12
|
+
def systemd_timer_lines
|
|
13
|
+
raise NotImplementedError, "#{self.class} must implement #systemd_timer_lines"
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# @return [String] stable signature for unit naming
|
|
17
|
+
def signature
|
|
18
|
+
raise NotImplementedError, "#{self.class} must implement #signature"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -4,23 +4,25 @@ module Wheneverd
|
|
|
4
4
|
module Trigger
|
|
5
5
|
# A boot trigger, rendered as `OnBootSec=`.
|
|
6
6
|
class Boot
|
|
7
|
+
include Base
|
|
8
|
+
|
|
7
9
|
# @return [Integer]
|
|
8
10
|
attr_reader :seconds
|
|
9
11
|
|
|
10
12
|
# @param seconds [Integer] seconds after boot (must be positive)
|
|
11
13
|
def initialize(seconds:)
|
|
12
|
-
|
|
13
|
-
raise ArgumentError, "Boot seconds must be an Integer (got #{seconds.class})"
|
|
14
|
-
end
|
|
15
|
-
raise ArgumentError, "Boot seconds must be positive (got #{seconds})" if seconds <= 0
|
|
16
|
-
|
|
17
|
-
@seconds = seconds
|
|
14
|
+
@seconds = Validation.positive_integer(seconds, name: "Boot seconds")
|
|
18
15
|
end
|
|
19
16
|
|
|
20
17
|
# @return [Array<String>] systemd `[Timer]` lines for this trigger
|
|
21
18
|
def systemd_timer_lines
|
|
22
19
|
["OnBootSec=#{seconds}"]
|
|
23
20
|
end
|
|
21
|
+
|
|
22
|
+
# @return [String] stable signature for unit naming
|
|
23
|
+
def signature
|
|
24
|
+
"boot:#{seconds}"
|
|
25
|
+
end
|
|
24
26
|
end
|
|
25
27
|
end
|
|
26
28
|
end
|
|
@@ -4,6 +4,8 @@ module Wheneverd
|
|
|
4
4
|
module Trigger
|
|
5
5
|
# A calendar trigger, rendered as one or more `OnCalendar=` lines.
|
|
6
6
|
class Calendar
|
|
7
|
+
include Base
|
|
8
|
+
|
|
7
9
|
# @return [Array<String>] calendar specs (already in `systemd` OnCalendar format)
|
|
8
10
|
attr_reader :on_calendar
|
|
9
11
|
|
|
@@ -21,6 +23,11 @@ module Wheneverd
|
|
|
21
23
|
def systemd_timer_lines
|
|
22
24
|
on_calendar.map { |spec| "OnCalendar=#{spec}" }
|
|
23
25
|
end
|
|
26
|
+
|
|
27
|
+
# @return [String] stable signature for unit naming
|
|
28
|
+
def signature
|
|
29
|
+
"calendar:#{on_calendar.sort.join('|')}"
|
|
30
|
+
end
|
|
24
31
|
end
|
|
25
32
|
end
|
|
26
33
|
end
|
|
@@ -8,23 +8,25 @@ module Wheneverd
|
|
|
8
8
|
# - `OnActiveSec=` to schedule the first run relative to timer activation.
|
|
9
9
|
# - `OnUnitActiveSec=` to schedule subsequent runs relative to the last run.
|
|
10
10
|
class Interval
|
|
11
|
+
include Base
|
|
12
|
+
|
|
11
13
|
# @return [Integer]
|
|
12
14
|
attr_reader :seconds
|
|
13
15
|
|
|
14
16
|
# @param seconds [Integer] seconds between runs (must be positive)
|
|
15
17
|
def initialize(seconds:)
|
|
16
|
-
|
|
17
|
-
raise ArgumentError, "Interval seconds must be an Integer (got #{seconds.class})"
|
|
18
|
-
end
|
|
19
|
-
raise ArgumentError, "Interval seconds must be positive (got #{seconds})" if seconds <= 0
|
|
20
|
-
|
|
21
|
-
@seconds = seconds
|
|
18
|
+
@seconds = Validation.positive_integer(seconds, name: "Interval seconds")
|
|
22
19
|
end
|
|
23
20
|
|
|
24
21
|
# @return [Array<String>] systemd `[Timer]` lines for this trigger
|
|
25
22
|
def systemd_timer_lines
|
|
26
23
|
["OnActiveSec=#{seconds}", "OnUnitActiveSec=#{seconds}"]
|
|
27
24
|
end
|
|
25
|
+
|
|
26
|
+
# @return [String] stable signature for unit naming
|
|
27
|
+
def signature
|
|
28
|
+
"interval:#{seconds}"
|
|
29
|
+
end
|
|
28
30
|
end
|
|
29
31
|
end
|
|
30
32
|
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Wheneverd
|
|
4
|
+
# Common validation utilities for consistent error handling across the codebase.
|
|
5
|
+
#
|
|
6
|
+
# These validators follow a consistent pattern:
|
|
7
|
+
# - Return the validated value if valid
|
|
8
|
+
# - Raise an appropriate error with a descriptive message if invalid
|
|
9
|
+
#
|
|
10
|
+
# @example Type validation
|
|
11
|
+
# Validation.type(value, Integer, name: "seconds")
|
|
12
|
+
# # => value or raises ArgumentError
|
|
13
|
+
#
|
|
14
|
+
# @example Positive integer validation
|
|
15
|
+
# Validation.positive_integer(value, name: "seconds")
|
|
16
|
+
# # => value or raises ArgumentError
|
|
17
|
+
module Validation
|
|
18
|
+
# Validate that a value is of the expected type.
|
|
19
|
+
#
|
|
20
|
+
# @param value [Object] the value to validate
|
|
21
|
+
# @param expected_type [Class, Module] the expected type
|
|
22
|
+
# @param name [String] the parameter name for error messages
|
|
23
|
+
# @return [Object] the validated value
|
|
24
|
+
# @raise [ArgumentError] if the value is not of the expected type
|
|
25
|
+
def self.type(value, expected_type, name:)
|
|
26
|
+
return value if value.is_a?(expected_type)
|
|
27
|
+
|
|
28
|
+
raise ArgumentError,
|
|
29
|
+
"#{name} must be #{expected_type_name(expected_type)} (got #{value.class})"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Validate that a value is a positive integer.
|
|
33
|
+
#
|
|
34
|
+
# @param value [Object] the value to validate
|
|
35
|
+
# @param name [String] the parameter name for error messages
|
|
36
|
+
# @return [Integer] the validated value
|
|
37
|
+
# @raise [ArgumentError] if the value is not a positive integer
|
|
38
|
+
def self.positive_integer(value, name:)
|
|
39
|
+
type(value, Integer, name: name)
|
|
40
|
+
return value if value.positive?
|
|
41
|
+
|
|
42
|
+
raise ArgumentError, "#{name} must be positive (got #{value})"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Validate that a string is non-empty after stripping whitespace.
|
|
46
|
+
#
|
|
47
|
+
# @param value [String] the value to validate
|
|
48
|
+
# @param name [String] the parameter name for error messages
|
|
49
|
+
# @return [String] the stripped value
|
|
50
|
+
# @raise [ArgumentError] if the value is empty after stripping
|
|
51
|
+
def self.non_empty_string(value, name:)
|
|
52
|
+
stripped = value.to_s.strip
|
|
53
|
+
return stripped unless stripped.empty?
|
|
54
|
+
|
|
55
|
+
raise ArgumentError, "#{name} must not be empty"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Validate that an array is non-empty.
|
|
59
|
+
#
|
|
60
|
+
# @param value [Array] the value to validate
|
|
61
|
+
# @param name [String] the parameter name for error messages
|
|
62
|
+
# @return [Array] the validated value
|
|
63
|
+
# @raise [ArgumentError] if the array is empty
|
|
64
|
+
def self.non_empty_array(value, name:)
|
|
65
|
+
type(value, Array, name: name)
|
|
66
|
+
return value unless value.empty?
|
|
67
|
+
|
|
68
|
+
raise ArgumentError, "#{name} must not be empty"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Validate that a value is within a range.
|
|
72
|
+
#
|
|
73
|
+
# @param value [Comparable] the value to validate
|
|
74
|
+
# @param range [Range] the valid range
|
|
75
|
+
# @param name [String] the parameter name for error messages
|
|
76
|
+
# @return [Comparable] the validated value
|
|
77
|
+
# @raise [ArgumentError] if the value is outside the range
|
|
78
|
+
def self.in_range(value, range, name:)
|
|
79
|
+
return value if range.cover?(value)
|
|
80
|
+
|
|
81
|
+
raise ArgumentError, "#{name} must be in #{range} (got #{value})"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self.expected_type_name(expected_type)
|
|
85
|
+
"a #{expected_type}"
|
|
86
|
+
end
|
|
87
|
+
private_class_method :expected_type_name
|
|
88
|
+
end
|
|
89
|
+
end
|
data/lib/wheneverd/version.rb
CHANGED
data/lib/wheneverd.rb
CHANGED
|
@@ -11,14 +11,15 @@ module Wheneverd
|
|
|
11
11
|
# - {Wheneverd::DSL::Loader} for evaluating `config/schedule.rb`
|
|
12
12
|
# - {Wheneverd::Systemd::Renderer} for generating unit contents
|
|
13
13
|
# - {Wheneverd::CLI} for the command-line interface
|
|
14
|
-
class Error < StandardError; end
|
|
15
14
|
end
|
|
16
15
|
|
|
17
16
|
require_relative "wheneverd/errors"
|
|
17
|
+
require_relative "wheneverd/validation"
|
|
18
18
|
require_relative "wheneverd/duration"
|
|
19
19
|
require_relative "wheneverd/interval"
|
|
20
20
|
require_relative "wheneverd/core_ext/numeric_duration"
|
|
21
21
|
require_relative "wheneverd/job/command"
|
|
22
|
+
require_relative "wheneverd/trigger/base"
|
|
22
23
|
require_relative "wheneverd/trigger/interval"
|
|
23
24
|
require_relative "wheneverd/trigger/calendar"
|
|
24
25
|
require_relative "wheneverd/trigger/boot"
|
|
@@ -30,10 +31,12 @@ require_relative "wheneverd/dsl/period_parser"
|
|
|
30
31
|
require_relative "wheneverd/dsl/context"
|
|
31
32
|
require_relative "wheneverd/dsl/loader"
|
|
32
33
|
require_relative "wheneverd/systemd/errors"
|
|
34
|
+
require_relative "wheneverd/systemd/unit_path_utils"
|
|
33
35
|
require_relative "wheneverd/systemd/time_parser"
|
|
34
36
|
require_relative "wheneverd/systemd/cron_parser"
|
|
35
37
|
require_relative "wheneverd/systemd/calendar_spec"
|
|
36
38
|
require_relative "wheneverd/systemd/unit_namer"
|
|
39
|
+
require_relative "wheneverd/systemd/unit_content_builder"
|
|
37
40
|
require_relative "wheneverd/systemd/renderer"
|
|
38
41
|
require_relative "wheneverd/systemd/analyze"
|
|
39
42
|
require_relative "wheneverd/systemd/systemctl"
|
data/test/domain_model_test.rb
CHANGED
|
@@ -105,4 +105,109 @@ class DomainModelTest < Minitest::Test
|
|
|
105
105
|
def test_entry_requires_a_trigger
|
|
106
106
|
assert_raises(ArgumentError) { Wheneverd::Entry.new(trigger: nil) }
|
|
107
107
|
end
|
|
108
|
+
|
|
109
|
+
def test_trigger_base_module_raises_not_implemented
|
|
110
|
+
# Create a class that includes Base but doesn't implement the methods
|
|
111
|
+
test_trigger_class = Class.new do
|
|
112
|
+
include Wheneverd::Trigger::Base
|
|
113
|
+
end
|
|
114
|
+
trigger = test_trigger_class.new
|
|
115
|
+
|
|
116
|
+
error = assert_raises(NotImplementedError) { trigger.systemd_timer_lines }
|
|
117
|
+
assert_includes error.message, "must implement #systemd_timer_lines"
|
|
118
|
+
|
|
119
|
+
error = assert_raises(NotImplementedError) { trigger.signature }
|
|
120
|
+
assert_includes error.message, "must implement #signature"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def test_trigger_signatures
|
|
124
|
+
interval = Wheneverd::Trigger::Interval.new(seconds: 60)
|
|
125
|
+
assert_equal "interval:60", interval.signature
|
|
126
|
+
|
|
127
|
+
boot = Wheneverd::Trigger::Boot.new(seconds: 5)
|
|
128
|
+
assert_equal "boot:5", boot.signature
|
|
129
|
+
|
|
130
|
+
calendar = Wheneverd::Trigger::Calendar.new(on_calendar: %w[daily hourly])
|
|
131
|
+
assert_equal "calendar:daily|hourly", calendar.signature
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def test_period_strategy_base_raises_not_implemented
|
|
135
|
+
# Create a class that inherits from Base but doesn't implement the methods
|
|
136
|
+
test_strategy_class = Class.new(Wheneverd::DSL::PeriodStrategy::Base)
|
|
137
|
+
strategy = test_strategy_class.new(path: "test")
|
|
138
|
+
|
|
139
|
+
error = assert_raises(NotImplementedError) { strategy.handles?(:anything) }
|
|
140
|
+
assert_includes error.message, "must implement #handles?"
|
|
141
|
+
|
|
142
|
+
error = assert_raises(NotImplementedError) { strategy.parse(:anything, at_times: []) }
|
|
143
|
+
assert_includes error.message, "must implement #parse"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def test_validation_type
|
|
147
|
+
# Valid type
|
|
148
|
+
assert_equal 42, Wheneverd::Validation.type(42, Integer, name: "value")
|
|
149
|
+
|
|
150
|
+
# Invalid type
|
|
151
|
+
error = assert_raises(ArgumentError) do
|
|
152
|
+
Wheneverd::Validation.type("string", Integer, name: "value")
|
|
153
|
+
end
|
|
154
|
+
assert_includes error.message, "value must be a Integer"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def test_validation_positive_integer
|
|
158
|
+
# Valid positive integer
|
|
159
|
+
assert_equal 5, Wheneverd::Validation.positive_integer(5, name: "count")
|
|
160
|
+
|
|
161
|
+
# Non-positive
|
|
162
|
+
error = assert_raises(ArgumentError) do
|
|
163
|
+
Wheneverd::Validation.positive_integer(0, name: "count")
|
|
164
|
+
end
|
|
165
|
+
assert_includes error.message, "must be positive"
|
|
166
|
+
|
|
167
|
+
# Non-integer
|
|
168
|
+
error = assert_raises(ArgumentError) do
|
|
169
|
+
Wheneverd::Validation.positive_integer("5", name: "count")
|
|
170
|
+
end
|
|
171
|
+
assert_includes error.message, "must be a Integer"
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def test_validation_non_empty_string
|
|
175
|
+
# Valid non-empty string
|
|
176
|
+
assert_equal "hello", Wheneverd::Validation.non_empty_string(" hello ", name: "text")
|
|
177
|
+
|
|
178
|
+
# Empty string
|
|
179
|
+
error = assert_raises(ArgumentError) do
|
|
180
|
+
Wheneverd::Validation.non_empty_string(" ", name: "text")
|
|
181
|
+
end
|
|
182
|
+
assert_includes error.message, "must not be empty"
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def test_validation_non_empty_array
|
|
186
|
+
# Valid non-empty array
|
|
187
|
+
arr = [1, 2, 3]
|
|
188
|
+
assert_equal arr, Wheneverd::Validation.non_empty_array(arr, name: "items")
|
|
189
|
+
|
|
190
|
+
# Empty array
|
|
191
|
+
error = assert_raises(ArgumentError) do
|
|
192
|
+
Wheneverd::Validation.non_empty_array([], name: "items")
|
|
193
|
+
end
|
|
194
|
+
assert_includes error.message, "must not be empty"
|
|
195
|
+
|
|
196
|
+
# Non-array
|
|
197
|
+
error = assert_raises(ArgumentError) do
|
|
198
|
+
Wheneverd::Validation.non_empty_array("not an array", name: "items")
|
|
199
|
+
end
|
|
200
|
+
assert_includes error.message, "must be a Array"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def test_validation_in_range
|
|
204
|
+
# Valid in range
|
|
205
|
+
assert_equal 5, Wheneverd::Validation.in_range(5, 1..10, name: "value")
|
|
206
|
+
|
|
207
|
+
# Out of range
|
|
208
|
+
error = assert_raises(ArgumentError) do
|
|
209
|
+
Wheneverd::Validation.in_range(15, 1..10, name: "value")
|
|
210
|
+
end
|
|
211
|
+
assert_includes error.message, "must be in 1..10"
|
|
212
|
+
end
|
|
108
213
|
end
|
|
@@ -38,28 +38,51 @@ class SystemdCronParserTest < Minitest::Test
|
|
|
38
38
|
end
|
|
39
39
|
end
|
|
40
40
|
|
|
41
|
-
def
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
41
|
+
def test_field_parser_rejects_empty_numeric_field
|
|
42
|
+
assert_raises(Wheneverd::Systemd::UnsupportedCronError) do
|
|
43
|
+
Wheneverd::Systemd::CronParser::FieldParser.parse_mapped(
|
|
44
|
+
"",
|
|
45
|
+
0..59,
|
|
46
|
+
field: "minute",
|
|
47
|
+
input: "x",
|
|
48
|
+
names: {}
|
|
49
|
+
)
|
|
50
|
+
end
|
|
50
51
|
end
|
|
51
52
|
|
|
52
|
-
def
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
def test_field_parser_rejects_invalid_numeric_tokens
|
|
54
|
+
# Empty token (just whitespace)
|
|
55
|
+
assert_raises(Wheneverd::Systemd::UnsupportedCronError) do
|
|
56
|
+
Wheneverd::Systemd::CronParser::FieldParser.parse_mapped(
|
|
57
|
+
" ",
|
|
58
|
+
0..59,
|
|
59
|
+
field: "minute",
|
|
60
|
+
input: "x",
|
|
61
|
+
names: {}
|
|
62
|
+
)
|
|
63
|
+
end
|
|
56
64
|
end
|
|
57
65
|
|
|
58
|
-
def
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
def test_dow_parser_rejects_empty_and_invalid_tokens
|
|
67
|
+
# Empty day-of-week field
|
|
68
|
+
assert_raises(Wheneverd::Systemd::UnsupportedCronError) do
|
|
69
|
+
Wheneverd::Systemd::CronParser::DowParser.parse("", input: "x")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Out of range day-of-week value (8)
|
|
73
|
+
assert_raises(Wheneverd::Systemd::UnsupportedCronError) do
|
|
74
|
+
Wheneverd::Systemd::CronParser::DowParser.parse("8", input: "x")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Invalid step (non-numeric)
|
|
78
|
+
assert_raises(Wheneverd::Systemd::UnsupportedCronError) do
|
|
79
|
+
Wheneverd::Systemd::CronParser::DowParser.parse("*/x", input: "x")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Invalid step (zero)
|
|
83
|
+
assert_raises(Wheneverd::Systemd::UnsupportedCronError) do
|
|
84
|
+
Wheneverd::Systemd::CronParser::DowParser.parse("*/0", input: "x")
|
|
85
|
+
end
|
|
63
86
|
end
|
|
64
87
|
|
|
65
88
|
def test_rejects_unsupported_cron_patterns
|
|
@@ -97,13 +120,6 @@ class SystemdCronParserTest < Minitest::Test
|
|
|
97
120
|
assert_equal expected, Wheneverd::Systemd::CronParser.to_on_calendar_values(cron)
|
|
98
121
|
end
|
|
99
122
|
|
|
100
|
-
def assert_private_unsupported(method, *args, **kwargs)
|
|
101
|
-
parser = Wheneverd::Systemd::CronParser
|
|
102
|
-
assert_raises(Wheneverd::Systemd::UnsupportedCronError) do
|
|
103
|
-
parser.send(method, *args, **kwargs)
|
|
104
|
-
end
|
|
105
|
-
end
|
|
106
|
-
|
|
107
123
|
def assert_unsupported(*crons)
|
|
108
124
|
crons.each do |cron|
|
|
109
125
|
assert_raises(Wheneverd::Systemd::UnsupportedCronError) do
|
|
@@ -44,7 +44,7 @@ class SystemdRendererErrorsTest < Minitest::Test
|
|
|
44
44
|
|
|
45
45
|
def test_timer_lines_for_rejects_unknown_trigger
|
|
46
46
|
assert_raises(ArgumentError) do
|
|
47
|
-
Wheneverd::Systemd::
|
|
47
|
+
Wheneverd::Systemd::UnitContentBuilder.timer_lines_for(Object.new)
|
|
48
48
|
end
|
|
49
49
|
end
|
|
50
50
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: wheneverd
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- bigcurl
|
|
@@ -66,6 +66,12 @@ files:
|
|
|
66
66
|
- lib/wheneverd/dsl/errors.rb
|
|
67
67
|
- lib/wheneverd/dsl/loader.rb
|
|
68
68
|
- lib/wheneverd/dsl/period_parser.rb
|
|
69
|
+
- lib/wheneverd/dsl/period_strategy.rb
|
|
70
|
+
- lib/wheneverd/dsl/period_strategy/array_strategy.rb
|
|
71
|
+
- lib/wheneverd/dsl/period_strategy/base.rb
|
|
72
|
+
- lib/wheneverd/dsl/period_strategy/duration_strategy.rb
|
|
73
|
+
- lib/wheneverd/dsl/period_strategy/string_strategy.rb
|
|
74
|
+
- lib/wheneverd/dsl/period_strategy/symbol_strategy.rb
|
|
69
75
|
- lib/wheneverd/duration.rb
|
|
70
76
|
- lib/wheneverd/entry.rb
|
|
71
77
|
- lib/wheneverd/errors.rb
|
|
@@ -75,18 +81,24 @@ files:
|
|
|
75
81
|
- lib/wheneverd/systemd/analyze.rb
|
|
76
82
|
- lib/wheneverd/systemd/calendar_spec.rb
|
|
77
83
|
- lib/wheneverd/systemd/cron_parser.rb
|
|
84
|
+
- lib/wheneverd/systemd/cron_parser/dow_parser.rb
|
|
85
|
+
- lib/wheneverd/systemd/cron_parser/field_parser.rb
|
|
78
86
|
- lib/wheneverd/systemd/errors.rb
|
|
79
87
|
- lib/wheneverd/systemd/loginctl.rb
|
|
80
88
|
- lib/wheneverd/systemd/renderer.rb
|
|
81
89
|
- lib/wheneverd/systemd/systemctl.rb
|
|
82
90
|
- lib/wheneverd/systemd/time_parser.rb
|
|
91
|
+
- lib/wheneverd/systemd/unit_content_builder.rb
|
|
83
92
|
- lib/wheneverd/systemd/unit_deleter.rb
|
|
84
93
|
- lib/wheneverd/systemd/unit_lister.rb
|
|
85
94
|
- lib/wheneverd/systemd/unit_namer.rb
|
|
95
|
+
- lib/wheneverd/systemd/unit_path_utils.rb
|
|
86
96
|
- lib/wheneverd/systemd/unit_writer.rb
|
|
97
|
+
- lib/wheneverd/trigger/base.rb
|
|
87
98
|
- lib/wheneverd/trigger/boot.rb
|
|
88
99
|
- lib/wheneverd/trigger/calendar.rb
|
|
89
100
|
- lib/wheneverd/trigger/interval.rb
|
|
101
|
+
- lib/wheneverd/validation.rb
|
|
90
102
|
- lib/wheneverd/version.rb
|
|
91
103
|
- test/cli_activate_test.rb
|
|
92
104
|
- test/cli_current_test.rb
|