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.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/README.md +45 -2
  4. data/lib/wheneverd/dsl/period_parser.rb +8 -107
  5. data/lib/wheneverd/dsl/period_strategy/array_strategy.rb +29 -0
  6. data/lib/wheneverd/dsl/period_strategy/base.rb +65 -0
  7. data/lib/wheneverd/dsl/period_strategy/duration_strategy.rb +33 -0
  8. data/lib/wheneverd/dsl/period_strategy/string_strategy.rb +51 -0
  9. data/lib/wheneverd/dsl/period_strategy/symbol_strategy.rb +31 -0
  10. data/lib/wheneverd/dsl/period_strategy.rb +43 -0
  11. data/lib/wheneverd/duration.rb +1 -7
  12. data/lib/wheneverd/errors.rb +3 -0
  13. data/lib/wheneverd/interval.rb +22 -7
  14. data/lib/wheneverd/systemd/cron_parser/dow_parser.rb +208 -0
  15. data/lib/wheneverd/systemd/cron_parser/field_parser.rb +163 -0
  16. data/lib/wheneverd/systemd/cron_parser.rb +56 -303
  17. data/lib/wheneverd/systemd/renderer.rb +6 -64
  18. data/lib/wheneverd/systemd/unit_content_builder.rb +76 -0
  19. data/lib/wheneverd/systemd/unit_deleter.rb +2 -28
  20. data/lib/wheneverd/systemd/unit_lister.rb +2 -28
  21. data/lib/wheneverd/systemd/unit_namer.rb +6 -14
  22. data/lib/wheneverd/systemd/unit_path_utils.rb +54 -0
  23. data/lib/wheneverd/systemd/unit_writer.rb +2 -28
  24. data/lib/wheneverd/trigger/base.rb +22 -0
  25. data/lib/wheneverd/trigger/boot.rb +8 -6
  26. data/lib/wheneverd/trigger/calendar.rb +7 -0
  27. data/lib/wheneverd/trigger/interval.rb +8 -6
  28. data/lib/wheneverd/validation.rb +89 -0
  29. data/lib/wheneverd/version.rb +1 -1
  30. data/lib/wheneverd.rb +4 -1
  31. data/test/domain_model_test.rb +105 -0
  32. data/test/systemd_cron_parser_test.rb +41 -25
  33. data/test/systemd_renderer_errors_test.rb +1 -1
  34. 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
- case trigger
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
- case job
55
- when Wheneverd::Job::Command
56
- job.signature
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
- unless seconds.is_a?(Integer)
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
- unless seconds.is_a?(Integer)
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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Wheneverd
4
4
  # Gem version.
5
- VERSION = "0.3.0"
5
+ VERSION = "0.4.0"
6
6
  end
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"
@@ -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 test_private_helpers_reject_empty_numeric_field
42
- assert_private_unsupported(
43
- :parse_mapped_numeric_expression,
44
- "",
45
- 0..59,
46
- field: "minute",
47
- input: "x",
48
- names: {}
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 test_private_helpers_reject_invalid_numeric_tokens
53
- common = { field: "minute", input: "x", names: {} }
54
- assert_private_unsupported(:parse_mapped_value, " ", 0..59, **common)
55
- assert_private_unsupported(:parse_positive_int, "x", field: "minute", input: "x", label: "step")
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 test_private_helpers_reject_empty_dow_and_invalid_tokens
59
- assert_private_unsupported(:parse_dow_set, "", input: "x")
60
- assert_private_unsupported(:apply_dow_part, Array.new(7, false), "", input: "x")
61
- assert_private_unsupported(:parse_dow_value, "", input: "x")
62
- assert_private_unsupported(:parse_dow_value, "8", input: "x")
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::Renderer.send(:timer_lines_for, Object.new)
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.3.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