wheneverd 0.2.1 → 0.3.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: 7ef1858fab03a38b36da00bf0093d4025762263ca868d05a78c1876d8aeb6441
4
- data.tar.gz: 539fbeec1db7023059767501172d7337da50a966e0727c29879f88cd81770303
3
+ metadata.gz: c4d813e1e0ad0b44e738d4ba42767b5fb490dcaf5a7883328c91faa2ab726305
4
+ data.tar.gz: 1185929834d9f12adb4afd8a1bc2d1a4a366d99b260afe1d075a1ca58dc6dc26
5
5
  SHA512:
6
- metadata.gz: 2656962166f254b2d48f94d23329db78a16964eda9c164457c5586b4dabfa095077b75372b94da9f9cad38862d5548be15ba7b2567cef5d2fb2cdc3cb94d4984
7
- data.tar.gz: a06b649a079b18fd3f321bc818e322d3284c98d96bfbabaf43244b4d604193b395186d31548efdf2296fe12509f47e665f1cdf96f47e2af25512b3eae53ba20d
6
+ metadata.gz: ed43947461d6e8dadf5825c8075e08816535c42202a9764c7b57d1fb4a7948a0dbfe1ff57e13cc710a853efa6910f802b93774b239736ed30bcec145812c08fc
7
+ data.tar.gz: 4406adc4f70afde1953cd8ea82979d2c37bf6b096232930724f2537b04d5ae187437fafa29215163ebd985eb132b89c7ee990497ba2cdb70617166b5d88114ee
data/CHANGELOG.md CHANGED
@@ -5,6 +5,12 @@ On release, entries are moved into `## x.y.z` sections that match the gem versio
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 0.3.0
9
+
10
+ - Schedule DSL: `command` accepts argv arrays, adds a `shell` helper for `/bin/bash -lc`, and `wheneverd init` includes examples.
11
+ - Adds `wheneverd status` (show `systemctl --user list-timers` + `systemctl --user status` for installed timers) and `wheneverd diff` (diff rendered units vs files on disk).
12
+ - Adds `wheneverd validate` to validate rendered `OnCalendar=` values via `systemd-analyze calendar` (and with `--verify`, runs `systemd-analyze --user verify` on temporary unit files).
13
+
8
14
  ## 0.2.1
9
15
 
10
16
  - Removes an unused filtering metadata keyword argument from the schedule DSL.
data/FEATURE_SUMMARY.md CHANGED
@@ -12,13 +12,22 @@ It complements [`CHANGELOG.md`](CHANGELOG.md) by staying high-level and focusing
12
12
 
13
13
  ## Unreleased
14
14
 
15
+ ## 0.3.0
16
+
17
+ - Schedule DSL: `command` accepts argv arrays, adds a `shell` helper for `/bin/bash -lc`, and `wheneverd init` includes examples.
18
+ - Adds `wheneverd status` (show `systemctl --user list-timers` + `systemctl --user status` for installed timers) and `wheneverd diff` (diff rendered units vs files on disk).
19
+ - Adds `wheneverd validate` to validate rendered `OnCalendar=` values via `systemd-analyze calendar` (and with `--verify`, runs `systemd-analyze --user verify` on temporary unit files).
20
+
15
21
  ## 0.2.1
16
22
 
17
- - Adds `wheneverd linger enable|disable|status` for managing systemd user lingering via `loginctl`.
18
23
  - Removes an unused filtering metadata keyword argument from the schedule DSL.
19
24
 
20
25
  ## 0.2.0
21
26
 
27
+ - Adds `wheneverd linger enable|disable|status` for managing systemd user lingering via `loginctl`.
28
+
29
+ ## 0.1.0
30
+
22
31
  - The `wheneverd` CLI is implemented using Clamp (`--help`, usage errors in `ERROR: ...` format, `--verbose` for details).
23
32
  - The gem includes a small “whenever-like” domain model (interval parsing, durations, triggers, schedules).
24
33
  - The gem can load a Ruby schedule DSL file via `Wheneverd::DSL::Loader.load_file`.
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- wheneverd (0.2.0)
4
+ wheneverd (0.3.0)
5
5
  clamp (~> 1.3)
6
6
 
7
7
  GEM
@@ -122,7 +122,7 @@ CHECKSUMS
122
122
  tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
123
123
  unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
124
124
  unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
125
- wheneverd (0.2.0)
125
+ wheneverd (0.3.0)
126
126
  yard (0.9.38) sha256=721fb82afb10532aa49860655f6cc2eaa7130889df291b052e1e6b268283010f
127
127
 
128
128
  BUNDLED WITH
data/README.md CHANGED
@@ -34,6 +34,9 @@ bundle install
34
34
  wheneverd --help
35
35
  wheneverd init
36
36
  wheneverd show
37
+ wheneverd status
38
+ wheneverd diff
39
+ wheneverd validate
37
40
  wheneverd write
38
41
  wheneverd delete
39
42
  wheneverd activate
@@ -43,6 +46,8 @@ wheneverd current
43
46
  wheneverd linger
44
47
  ```
45
48
 
49
+ Use `wheneverd init` to create a starter `config/schedule.rb` template (including examples for `command` and `shell`).
50
+
46
51
  ### Minimal `config/schedule.rb` example
47
52
 
48
53
  ```ruby
@@ -137,13 +142,26 @@ end
137
142
 
138
143
  ### `command`
139
144
 
140
- `command("...")` appends a oneshot `ExecStart=` job. Commands must be non-empty strings.
145
+ `command(...)` appends a oneshot `ExecStart=` job.
146
+
147
+ Accepted forms:
141
148
 
142
- The command string is inserted into `ExecStart=` as-is (no shell wrapping). If you need shell features
143
- (pipes, redirects, globbing, env var expansion), wrap it yourself, for example:
149
+ - `command("...")` (String): inserted into `ExecStart=` as-is (after stripping surrounding whitespace).
150
+ - `command(["bin", "arg1", "arg2"])` (argv Array): formatted/escaped into a systemd-compatible `ExecStart=` string.
151
+
152
+ If you need shell features (pipes, redirects, globbing, env var expansion), either wrap it yourself, or use `shell`:
144
153
 
145
154
  ```ruby
146
155
  command "/bin/bash -lc 'echo hello | sed -e s/hello/hi/'"
156
+ command ["/bin/bash", "-lc", "echo hello | sed -e s/hello/hi/"]
157
+ ```
158
+
159
+ ### `shell`
160
+
161
+ `shell("...")` is a convenience helper for the common `/bin/bash -lc` pattern:
162
+
163
+ ```ruby
164
+ shell "echo hello | sed -e s/hello/hi/"
147
165
  ```
148
166
 
149
167
  ### `every` periods
@@ -203,11 +221,15 @@ Notes:
203
221
  - Unit basenames include a stable ID derived from the job’s trigger + command (reordering schedule blocks won’t rename units).
204
222
  - `wheneverd write` / `wheneverd reload` prune previously generated units for the identifier by default (use `--no-prune` to keep old units around).
205
223
  - `--unit-dir` controls where unit files are written/read/deleted; `activate`/`deactivate` use systemd’s unit search path.
224
+ - `wheneverd diff` returns exit status `0` when no differences are found, and `1` when differences are found.
206
225
 
207
226
  Commands:
208
227
 
209
228
  - `wheneverd init [--schedule PATH] [--force]` writes a template schedule file.
210
229
  - `wheneverd show [--schedule PATH] [--identifier NAME]` prints rendered units to stdout.
230
+ - `wheneverd status [--identifier NAME] [--unit-dir PATH]` prints `systemctl --user list-timers` and `systemctl --user status` for installed timers.
231
+ - `wheneverd diff [--schedule PATH] [--identifier NAME] [--unit-dir PATH]` diffs rendered units vs unit files on disk.
232
+ - `wheneverd validate [--schedule PATH] [--identifier NAME] [--verify]` validates rendered `OnCalendar=` values via `systemd-analyze calendar` (and with `--verify`, runs `systemd-analyze --user verify` on temporary unit files).
211
233
  - `wheneverd write [--schedule PATH] [--identifier NAME] [--unit-dir PATH] [--dry-run] [--[no-]prune]` writes units to disk (or prints paths in `--dry-run` mode).
212
234
  - `wheneverd delete [--identifier NAME] [--unit-dir PATH] [--dry-run]` deletes previously generated units for the identifier.
213
235
  - `wheneverd activate [--schedule PATH] [--identifier NAME]` runs `systemctl --user daemon-reload` and enables/starts the timers.
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wheneverd
4
+ # Implements `wheneverd diff` (rendered output vs files on disk).
5
+ #
6
+ # Exit statuses follow `diff` semantics:
7
+ # - 0: no differences
8
+ # - 1: differences found
9
+ # - 2: error
10
+ class CLI::Diff < CLI
11
+ def execute
12
+ diffs = unit_diffs
13
+ return 0 if diffs.empty?
14
+
15
+ diffs.each_with_index do |diff, idx|
16
+ puts "" if idx.positive?
17
+ puts diff
18
+ end
19
+ 1
20
+ rescue StandardError => e
21
+ handle_error(e)
22
+ 2
23
+ end
24
+
25
+ private
26
+
27
+ def unit_diffs
28
+ rendered = rendered_units_by_basename
29
+ installed = installed_units_by_basename
30
+
31
+ diffs = rendered.map do |basename, contents|
32
+ diff_for_rendered_unit(basename, contents, installed[basename])
33
+ end
34
+ diffs.concat(stale_unit_diffs(rendered: rendered, installed: installed))
35
+ diffs.compact
36
+ end
37
+
38
+ def stale_unit_diffs(rendered:, installed:)
39
+ (installed.keys - rendered.keys).sort.map do |basename|
40
+ diff_for_removed_unit(basename, installed.fetch(basename))
41
+ end
42
+ end
43
+
44
+ def rendered_units_by_basename
45
+ render_units.to_h { |unit| [unit.path_basename, unit.contents.to_s] }
46
+ end
47
+
48
+ def installed_units_by_basename
49
+ paths = Wheneverd::Systemd::UnitLister.list(identifier: identifier_value, unit_dir: unit_dir)
50
+ paths.to_h { |path| [File.basename(path), path] }
51
+ end
52
+
53
+ def diff_for_rendered_unit(basename, rendered_contents, installed_path)
54
+ return diff_for_added_unit(basename, rendered_contents) if installed_path.nil?
55
+
56
+ installed_contents = File.read(installed_path)
57
+ return nil if installed_contents == rendered_contents
58
+
59
+ diff_for_changed_unit(basename, installed_path, installed_contents, rendered_contents)
60
+ end
61
+
62
+ def diff_for_added_unit(basename, rendered_contents)
63
+ dest_path = File.join(File.expand_path(unit_dir.to_s), basename)
64
+ header = ["diff --wheneverd #{basename}", "--- /dev/null", "+++ #{dest_path}"]
65
+ (header + line_diff("", rendered_contents)).join("\n")
66
+ end
67
+
68
+ def diff_for_removed_unit(basename, installed_path)
69
+ header = ["diff --wheneverd #{basename}", "--- #{installed_path}", "+++ /dev/null"]
70
+ (header + line_diff(File.read(installed_path), "")).join("\n")
71
+ end
72
+
73
+ def diff_for_changed_unit(basename, installed_path, installed_contents, rendered_contents)
74
+ header = ["diff --wheneverd #{basename}", "--- #{installed_path}",
75
+ "+++ #{basename} (rendered)"]
76
+ (header + line_diff(installed_contents, rendered_contents)).join("\n")
77
+ end
78
+
79
+ def line_diff(old_contents, new_contents)
80
+ old_lines = old_contents.to_s.lines.map(&:chomp)
81
+ new_lines = new_contents.to_s.lines.map(&:chomp)
82
+ max = [old_lines.length, new_lines.length].max
83
+
84
+ (0...max).flat_map do |idx|
85
+ diff_lines_for(old_lines[idx], new_lines[idx])
86
+ end
87
+ end
88
+
89
+ def diff_lines_for(old_line, new_line)
90
+ return [] if old_line.nil? && new_line.nil?
91
+ return [" #{old_line}"] if old_line == new_line
92
+ return ["-#{old_line}", "+#{new_line}"] if old_line && new_line
93
+ return ["-#{old_line}"] if old_line
94
+
95
+ ["+#{new_line}"]
96
+ end
97
+ end
98
+ end
@@ -32,7 +32,7 @@ module Wheneverd
32
32
  end
33
33
 
34
34
  every :hour do
35
- command "echo hourly"
35
+ command ["echo", "hello world"]
36
36
  end
37
37
 
38
38
  every :sunday, at: "12pm" do
@@ -43,6 +43,10 @@ module Wheneverd
43
43
  command "echo midweek"
44
44
  end
45
45
 
46
+ every :day, at: "12:15" do
47
+ shell "echo hello | sed -e s/hello/hi/"
48
+ end
49
+
46
50
  every "0 0 27-31 * *" do
47
51
  command "echo raw_cron"
48
52
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wheneverd
4
+ # Implements `wheneverd status` (show installed timer status via `systemctl --user`).
5
+ class CLI::Status < CLI
6
+ def execute
7
+ timer_units = installed_timer_unit_basenames
8
+ return 0 if timer_units.empty?
9
+
10
+ print_list_timers(timer_units)
11
+ print_status(timer_units)
12
+ 0
13
+ rescue StandardError => e
14
+ handle_error(e)
15
+ end
16
+
17
+ private
18
+
19
+ # @return [Array<String>]
20
+ def installed_timer_unit_basenames
21
+ paths = Wheneverd::Systemd::UnitLister.list(identifier: identifier_value, unit_dir: unit_dir)
22
+ paths.map { |path| File.basename(path) }.grep(/\.timer\z/).uniq
23
+ end
24
+
25
+ def print_list_timers(timer_units)
26
+ stdout, = Wheneverd::Systemd::Systemctl.run("list-timers", "--all", *timer_units)
27
+ print stdout
28
+ end
29
+
30
+ def print_status(timer_units)
31
+ timer_units.each do |unit|
32
+ stdout, = Wheneverd::Systemd::Systemctl.run("status", unit)
33
+ print stdout
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tmpdir"
4
+
5
+ module Wheneverd
6
+ # Implements `wheneverd validate` (validate rendered OnCalendar values and unit files).
7
+ class CLI::Validate < CLI
8
+ option "--verify", :flag,
9
+ "Also run systemd-analyze --user verify (writes units to a temporary directory)"
10
+
11
+ def execute
12
+ units = render_units
13
+ validate_on_calendar(units)
14
+ validate_units(units) if verify?
15
+ 0
16
+ rescue StandardError => e
17
+ handle_error(e)
18
+ end
19
+
20
+ private
21
+
22
+ def validate_on_calendar(units)
23
+ values = on_calendar_values(units)
24
+ if values.empty?
25
+ puts "No OnCalendar= values found" if verbose?
26
+ return
27
+ end
28
+
29
+ values.each do |value|
30
+ Wheneverd::Systemd::Analyze.calendar(value)
31
+ puts "OK OnCalendar=#{value}" if verbose?
32
+ end
33
+ end
34
+
35
+ def on_calendar_values(units)
36
+ units.select { |unit| unit.kind == :timer }
37
+ .flat_map { |timer| timer.contents.to_s.lines.grep(/\AOnCalendar=/) }
38
+ .map { |line| line.delete_prefix("OnCalendar=").strip }
39
+ .uniq
40
+ end
41
+
42
+ def validate_units(units)
43
+ Dir.mktmpdir("wheneverd-validate-") do |dir|
44
+ paths = Wheneverd::Systemd::UnitWriter.write(units, unit_dir: dir, prune: false)
45
+ Wheneverd::Systemd::Analyze.verify(paths, user: true)
46
+ puts "OK systemd-analyze --user verify" if verbose?
47
+ end
48
+ end
49
+ end
50
+ end
data/lib/wheneverd/cli.rb CHANGED
@@ -63,6 +63,9 @@ end
63
63
  require_relative "cli/help"
64
64
  require_relative "cli/init"
65
65
  require_relative "cli/show"
66
+ require_relative "cli/status"
67
+ require_relative "cli/diff"
68
+ require_relative "cli/validate"
66
69
  require_relative "cli/write"
67
70
  require_relative "cli/delete"
68
71
  require_relative "cli/activate"
@@ -78,6 +81,9 @@ module Wheneverd
78
81
  subcommand "help", "Show help", Wheneverd::CLI::Help
79
82
  subcommand "init", "Create a schedule template", Wheneverd::CLI::Init
80
83
  subcommand "show", "Render units to stdout", Wheneverd::CLI::Show
84
+ subcommand "status", "Show systemctl list-timers + status for this identifier", Wheneverd::CLI::Status
85
+ subcommand "diff", "Diff rendered units vs files on disk", Wheneverd::CLI::Diff
86
+ subcommand "validate", "Validate schedule via systemd-analyze", Wheneverd::CLI::Validate
81
87
  subcommand "write", "Write units to disk", Wheneverd::CLI::Write
82
88
  subcommand "delete", "Delete units from disk", Wheneverd::CLI::Delete
83
89
  subcommand "activate", "Enable and start timers via systemctl --user", Wheneverd::CLI::Activate
@@ -5,7 +5,7 @@ module Wheneverd
5
5
  # The evaluation context used for schedule files.
6
6
  #
7
7
  # The schedule file is evaluated via `instance_eval`, so methods defined here become available
8
- # as the schedule DSL (`every`, `command`).
8
+ # as the schedule DSL (`every`, `command`, `shell`).
9
9
  class Context
10
10
  # @return [String] absolute schedule path
11
11
  attr_reader :path
@@ -44,21 +44,64 @@ module Wheneverd
44
44
 
45
45
  # Add a oneshot command job to the current `every` entry.
46
46
  #
47
- # @param command_str [String]
47
+ # @example String command
48
+ # command "echo hello"
49
+ #
50
+ # @example argv command
51
+ # command ["echo", "hello world"]
52
+ #
53
+ # @param command_value [String, Array<String>]
48
54
  # @return [void]
49
- def command(command_str)
50
- unless @current_entry
51
- raise LoadError.new("command() must be called inside every() block",
52
- path: path)
53
- end
55
+ def command(command_value)
56
+ ensure_in_every_block!("command")
54
57
 
55
- @current_entry.add_job(Wheneverd::Job::Command.new(command: command_str))
58
+ @current_entry.add_job(Wheneverd::Job::Command.new(command: command_value))
56
59
  rescue Wheneverd::InvalidCommandError => e
57
60
  raise LoadError.new(e.message, path: path)
58
61
  end
59
62
 
63
+ # Add a oneshot command job that runs via `/bin/bash -lc`.
64
+ #
65
+ # @example
66
+ # shell "echo hello | sed -e s/hello/hi/"
67
+ #
68
+ # @param script [String] non-empty script to pass as `bash -lc <script>`
69
+ # @param shell [String] shell executable (default: "/bin/bash")
70
+ # @return [void]
71
+ def shell(script, shell: "/bin/bash")
72
+ ensure_in_every_block!("shell")
73
+ script_stripped = normalize_shell_script(script)
74
+ shell_executable = normalize_shell_executable(shell)
75
+ command([shell_executable, "-lc", script_stripped])
76
+ end
77
+
60
78
  private
61
79
 
80
+ def ensure_in_every_block!(name)
81
+ return if @current_entry
82
+
83
+ raise LoadError.new("#{name}() must be called inside every() block", path: path)
84
+ end
85
+
86
+ def normalize_shell_script(script)
87
+ unless script.is_a?(String)
88
+ raise LoadError.new("shell() script must be a String (got #{script.class})",
89
+ path: path)
90
+ end
91
+
92
+ stripped = script.strip
93
+ raise LoadError.new("shell() script must not be empty", path: path) if stripped.empty?
94
+
95
+ stripped
96
+ end
97
+
98
+ def normalize_shell_executable(shell)
99
+ stripped = shell.to_s.strip
100
+ raise LoadError.new("shell() shell must not be empty", path: path) if stripped.empty?
101
+
102
+ stripped
103
+ end
104
+
62
105
  def with_current_entry(entry)
63
106
  previous_entry = @current_entry
64
107
  @current_entry = entry
@@ -4,25 +4,105 @@ module Wheneverd
4
4
  module Job
5
5
  # A oneshot command job rendered as `ExecStart=` in a `systemd` service unit.
6
6
  #
7
- # Note that the command is inserted into `ExecStart=` as-is. If you need shell features like
8
- # pipes, redirects, or environment variable expansion, wrap the command explicitly:
7
+ # This job accepts either:
8
+ #
9
+ # - A String (inserted into `ExecStart=` as-is, after stripping surrounding whitespace), or
10
+ # - An argv Array (formatted/escaped into a systemd-compatible `ExecStart=` string).
11
+ #
12
+ # If you need shell features like pipes, redirects, or environment variable expansion, wrap
13
+ # the command explicitly:
9
14
  #
10
15
  # @example
11
16
  # command "/bin/bash -lc 'echo hello | sed -e s/hello/hi/'"
17
+ #
18
+ # @example argv form (safer argument handling)
19
+ # command ["/bin/bash", "-lc", "echo hello | sed -e s/hello/hi/"]
12
20
  class Command
21
+ SAFE_UNQUOTED = %r{\A[A-Za-z0-9_@%+=:,./-]+\z}.freeze
22
+
23
+ # Rendered `ExecStart=` value.
24
+ #
13
25
  # @return [String]
14
26
  attr_reader :command
15
27
 
16
- # @param command [String] non-empty command to run
28
+ # Original argv form (when constructed with an Array).
29
+ #
30
+ # @return [Array<String>, nil]
31
+ attr_reader :argv
32
+
33
+ # Stable signature used for unit naming.
34
+ #
35
+ # @return [String]
36
+ attr_reader :signature
37
+
38
+ # @param command [String, Array<String>] non-empty command to run
17
39
  def initialize(command:)
18
- unless command.is_a?(String)
19
- raise InvalidCommandError, "Command must be a String (got #{command.class})"
40
+ @argv = nil
41
+ @signature = nil
42
+ @command = nil
43
+
44
+ case command
45
+ when String then init_string(command)
46
+ when Array then init_argv(command)
47
+ else
48
+ raise InvalidCommandError,
49
+ "Command must be a String or an Array (got #{command.class})"
20
50
  end
51
+ end
52
+
53
+ private
54
+
55
+ def init_string(command)
56
+ stripped = command.strip
57
+ raise InvalidCommandError, "Command must not be empty" if stripped.empty?
58
+
59
+ @command = stripped
60
+ @signature = "command:#{@command}"
61
+ end
62
+
63
+ def init_argv(argv)
64
+ normalized = normalize_argv(argv)
65
+ @argv = normalized
66
+ @command = format_execstart(normalized)
67
+ @signature = ["command:argv", normalized.join("\n")].join("\n")
68
+ end
21
69
 
22
- command_stripped = command.strip
23
- raise InvalidCommandError, "Command must not be empty" if command_stripped.empty?
70
+ def normalize_argv(argv)
71
+ raise InvalidCommandError, "Command argv must not be empty" if argv.empty?
72
+
73
+ elements = argv.map { |arg| validate_argv_element(arg) }
74
+ elements[0] = elements[0].strip
75
+ raise InvalidCommandError, "Command argv executable must not be empty" if elements[0].empty?
76
+
77
+ elements
78
+ end
79
+
80
+ def validate_argv_element(arg)
81
+ unless arg.is_a?(String)
82
+ raise InvalidCommandError, "Command argv elements must be Strings (got #{arg.class})"
83
+ end
84
+
85
+ if arg.match?(/[\0\r\n]/)
86
+ raise InvalidCommandError,
87
+ "Command argv elements must not include NUL or newlines"
88
+ end
89
+
90
+ arg
91
+ end
92
+
93
+ def format_execstart(argv)
94
+ argv.map { |arg| format_exec_arg(arg) }.join(" ")
95
+ end
96
+
97
+ def format_exec_arg(arg)
98
+ return "\"\"" if arg.empty?
99
+ return arg if SAFE_UNQUOTED.match?(arg)
100
+
101
+ "\"#{escape_exec_arg(arg)}\""
102
+ end
24
103
 
25
- @command = command_stripped
104
+ def escape_exec_arg(arg)
105
+ arg.gsub(/[\\"]/) { |m| "\\#{m}" }
26
106
  end
27
107
  end
28
108
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module Wheneverd
6
+ module Systemd
7
+ # Thin wrapper around `systemd-analyze`.
8
+ class Analyze
9
+ DEFAULT_SYSTEMD_ANALYZE = "systemd-analyze"
10
+
11
+ # Run `systemd-analyze calendar <value>`.
12
+ #
13
+ # @param value [String] an `OnCalendar=` value
14
+ # @param systemd_analyze [String] path to the `systemd-analyze` executable
15
+ # @return [Array(String, String)] stdout and stderr
16
+ # @raise [Wheneverd::Systemd::SystemdAnalyzeError]
17
+ def self.calendar(value, systemd_analyze: DEFAULT_SYSTEMD_ANALYZE)
18
+ run(systemd_analyze, "calendar", value.to_s)
19
+ end
20
+
21
+ # Run `systemd-analyze verify` for unit files.
22
+ #
23
+ # @param paths [Array<String>] unit file paths to verify
24
+ # @param user [Boolean] verify user units with `--user` (default: true)
25
+ # @param systemd_analyze [String] path to the `systemd-analyze` executable
26
+ # @return [Array(String, String)] stdout and stderr
27
+ # @raise [Wheneverd::Systemd::SystemdAnalyzeError]
28
+ def self.verify(paths, user: true, systemd_analyze: DEFAULT_SYSTEMD_ANALYZE)
29
+ run(systemd_analyze, "verify", *Array(paths).map(&:to_s), user: user)
30
+ end
31
+
32
+ def self.run(systemd_analyze, *args, user: false)
33
+ cmd = [systemd_analyze.to_s]
34
+ cmd << "--user" if user
35
+ cmd.concat(args.flatten.map(&:to_s))
36
+
37
+ stdout, stderr, status = Open3.capture3(*cmd)
38
+ raise SystemdAnalyzeError, format_error(cmd, status, stdout, stderr) unless status.success?
39
+
40
+ [stdout, stderr]
41
+ rescue Errno::ENOENT
42
+ raise SystemdAnalyzeError, "systemd-analyze not found (tried: #{systemd_analyze})"
43
+ end
44
+
45
+ def self.format_error(cmd, status, stdout, stderr)
46
+ details = []
47
+ details << "command: #{cmd.join(' ')}"
48
+ details << "status: #{status.exitstatus}"
49
+ details << "stdout: #{stdout.strip}" unless stdout.to_s.strip.empty?
50
+ details << "stderr: #{stderr.strip}" unless stderr.to_s.strip.empty?
51
+ "systemd-analyze failed (#{details.join(', ')})"
52
+ end
53
+ private_class_method :format_error
54
+ end
55
+ end
56
+ end
@@ -22,5 +22,8 @@ module Wheneverd
22
22
 
23
23
  # Raised when a `loginctl` invocation fails.
24
24
  class LoginctlError < Error; end
25
+
26
+ # Raised when a `systemd-analyze` invocation fails (or `systemd-analyze` is not available).
27
+ class SystemdAnalyzeError < Error; end
25
28
  end
26
29
  end
@@ -53,7 +53,7 @@ module Wheneverd
53
53
  def self.job_signature(job)
54
54
  case job
55
55
  when Wheneverd::Job::Command
56
- "command:#{job.command}"
56
+ job.signature
57
57
  else
58
58
  raise ArgumentError, "Unsupported job type: #{job.class}"
59
59
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Wheneverd
4
4
  # Gem version.
5
- VERSION = "0.2.1"
5
+ VERSION = "0.3.0"
6
6
  end
data/lib/wheneverd.rb CHANGED
@@ -35,6 +35,7 @@ require_relative "wheneverd/systemd/cron_parser"
35
35
  require_relative "wheneverd/systemd/calendar_spec"
36
36
  require_relative "wheneverd/systemd/unit_namer"
37
37
  require_relative "wheneverd/systemd/renderer"
38
+ require_relative "wheneverd/systemd/analyze"
38
39
  require_relative "wheneverd/systemd/systemctl"
39
40
  require_relative "wheneverd/systemd/loginctl"
40
41
  require_relative "wheneverd/systemd/unit_writer"
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+ require_relative "support/cli_test_helpers"
5
+
6
+ class CLIDiffTest < Minitest::Test
7
+ include CLITestHelpers
8
+
9
+ def test_exits_one_and_shows_added_diff_when_units_are_not_installed
10
+ with_inited_unit_dir("missing_units") do |unit_dir|
11
+ timer = first_timer
12
+ status, out, err = run_diff(unit_dir)
13
+ assert_diff_added(status, out, err, unit_dir, timer)
14
+ end
15
+ end
16
+
17
+ def test_exits_zero_when_no_differences
18
+ with_installed_units do |unit_dir|
19
+ status, out, err = run_diff(unit_dir)
20
+ assert_equal 0, status
21
+ assert_equal "", err
22
+ assert_equal "", out
23
+ end
24
+ end
25
+
26
+ def test_exits_one_and_shows_removed_lines_when_installed_unit_was_modified
27
+ with_installed_units do |unit_dir|
28
+ timer = first_timer
29
+ File.open(File.join(unit_dir, timer), "a") { |f| f.puts "# local edit" }
30
+ status, out, err = run_diff(unit_dir)
31
+ assert_diff_contains(status, out, err, timer, "-# local edit")
32
+ end
33
+ end
34
+
35
+ def test_exits_one_and_shows_added_lines_when_installed_unit_is_missing_lines
36
+ with_installed_units do |unit_dir|
37
+ timer = first_timer
38
+ remove_exact_line(File.join(unit_dir, timer), "Persistent=true\n")
39
+ status, out, err = run_diff(unit_dir)
40
+ assert_diff_contains(status, out, err, timer, "+Persistent=true")
41
+ end
42
+ end
43
+
44
+ def test_exits_one_and_shows_diff_for_stale_units_on_disk
45
+ with_installed_units do |unit_dir|
46
+ stale_timer = "wheneverd-demo-000000000000.timer"
47
+ stale_path = write_stale_timer(unit_dir, stale_timer)
48
+ status, out, err = run_diff(unit_dir)
49
+ assert_diff_removed(status, out, err, stale_timer, stale_path)
50
+ end
51
+ end
52
+
53
+ def test_returns_two_on_error
54
+ with_project_dir do
55
+ status, out, err = run_cli(["diff", "--schedule", "missing.rb"])
56
+ assert_equal 2, status
57
+ assert_equal "", out
58
+ assert_includes err, "Schedule file not found"
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ def with_inited_unit_dir(name)
65
+ with_inited_project_dir do |project_dir|
66
+ yield File.join(project_dir, name)
67
+ end
68
+ end
69
+
70
+ def with_installed_units
71
+ with_inited_unit_dir("tmp_units") do |unit_dir|
72
+ assert_equal 0, run_cli(["write", "--identifier", "demo", "--unit-dir", unit_dir]).first
73
+ yield unit_dir
74
+ end
75
+ end
76
+
77
+ def run_diff(unit_dir)
78
+ run_cli(["diff", "--identifier", "demo", "--unit-dir", unit_dir])
79
+ end
80
+
81
+ def first_timer
82
+ expected_timer_basenames(identifier: "demo").fetch(0)
83
+ end
84
+
85
+ def remove_exact_line(path, line)
86
+ File.write(path, File.read(path).lines.reject { |l| l == line }.join)
87
+ end
88
+
89
+ def write_stale_timer(unit_dir, basename)
90
+ path = File.join(unit_dir, basename)
91
+ File.write(path, "#{Wheneverd::Systemd::Renderer::MARKER_PREFIX} test\n# stale\n")
92
+ path
93
+ end
94
+
95
+ def assert_diff_added(status, out, err, unit_dir, timer)
96
+ assert_equal 1, status
97
+ assert_equal "", err
98
+ assert_includes out, "diff --wheneverd #{timer}"
99
+ assert_includes out, "--- /dev/null"
100
+ assert_includes out, "+++ #{File.join(File.expand_path(unit_dir), timer)}"
101
+ end
102
+
103
+ def assert_diff_contains(status, out, err, timer, expected_line)
104
+ assert_equal 1, status
105
+ assert_equal "", err
106
+ assert_includes out, "diff --wheneverd #{timer}"
107
+ assert_includes out, expected_line
108
+ end
109
+
110
+ def assert_diff_removed(status, out, err, timer, path)
111
+ assert_equal 1, status
112
+ assert_equal "", err
113
+ assert_includes out, "diff --wheneverd #{timer}"
114
+ assert_includes out, "--- #{path}"
115
+ assert_includes out, "+++ /dev/null"
116
+ assert_includes out, "-# stale"
117
+ end
118
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+ require_relative "support/cli_test_helpers"
5
+
6
+ class CLIInitTest < Minitest::Test
7
+ include CLITestHelpers
8
+
9
+ def test_writes_template_with_shell_and_argv_examples
10
+ with_project_dir do |project_dir|
11
+ status, out, err = run_cli(["init"])
12
+ assert_equal 0, status
13
+ assert_equal "", err
14
+ assert_includes out, "Wrote schedule template to"
15
+
16
+ schedule_path = File.join(project_dir, "config", "schedule.rb")
17
+ schedule = File.read(schedule_path)
18
+ assert_includes schedule, "command [\"echo\", \"hello world\"]"
19
+ assert_includes schedule, "shell \"echo hello | sed -e s/hello/hi/\""
20
+ end
21
+ end
22
+
23
+ def test_refuses_to_overwrite_without_force
24
+ with_project_dir do
25
+ assert_equal 0, run_cli(["init"]).first
26
+
27
+ status, out, err = run_cli(["init"])
28
+ assert_equal 1, status
29
+ assert_equal "", out
30
+ assert_includes err, "already exists"
31
+ assert_includes err, "--force"
32
+ end
33
+ end
34
+
35
+ def test_overwrites_with_force
36
+ with_project_dir do |project_dir|
37
+ assert_equal 0, run_cli(["init"]).first
38
+
39
+ schedule_path = File.join(project_dir, "config", "schedule.rb")
40
+ File.write(schedule_path, "# custom\n")
41
+
42
+ status, out, err = run_cli(["init", "--force"])
43
+ assert_equal 0, status
44
+ assert_equal "", err
45
+ assert_includes out, "Overwrote schedule template to"
46
+ assert_includes File.read(schedule_path), "Supported `every` period forms:"
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+ require_relative "support/cli_test_helpers"
5
+
6
+ class CLIStatusTest < Minitest::Test
7
+ include CLITestHelpers
8
+
9
+ def test_runs_list_timers_and_status_for_each_installed_timer
10
+ with_installed_units do |unit_dir|
11
+ status, _out, err, calls = run_status(unit_dir)
12
+ assert_cli_success(status, err)
13
+ assert_status_calls(calls, expected_timer_units)
14
+ end
15
+ end
16
+
17
+ def test_returns_nonzero_when_systemctl_fails
18
+ with_installed_units do |unit_dir|
19
+ status, _out, err, calls = run_status(unit_dir, exitstatus: 1, stderr: "boom\n")
20
+ assert_equal 1, status
21
+ assert_includes err, "systemctl failed"
22
+ assert_includes err, "boom"
23
+ assert_includes calls.fetch(0).fetch(0), "list-timers"
24
+ end
25
+ end
26
+
27
+ def test_exits_zero_and_does_not_call_systemctl_when_no_timers_installed
28
+ with_project_dir do |project_dir|
29
+ unit_dir = File.join(project_dir, "empty_units")
30
+ FileUtils.mkdir_p(unit_dir)
31
+ status, out, err, calls = run_status(unit_dir)
32
+ assert_cli_success(status, err)
33
+ assert_equal "", out
34
+ assert_equal [], calls
35
+ end
36
+ end
37
+
38
+ private
39
+
40
+ def with_installed_units
41
+ with_inited_project_dir do |project_dir|
42
+ unit_dir = File.join(project_dir, "tmp_units")
43
+ assert_equal 0, run_cli(["write", "--identifier", "demo", "--unit-dir", unit_dir]).first
44
+ yield unit_dir
45
+ end
46
+ end
47
+
48
+ def run_status(unit_dir, **kwargs)
49
+ run_cli_with_capture3_stub(["status", "--identifier", "demo", "--unit-dir", unit_dir], **kwargs)
50
+ end
51
+
52
+ def expected_timer_units
53
+ expected_timer_basenames(identifier: "demo").sort
54
+ end
55
+
56
+ def assert_status_calls(calls, expected_timers)
57
+ assert_list_timers_call(calls, expected_timers)
58
+ assert_status_unit_calls(calls, expected_timers)
59
+ end
60
+
61
+ def assert_list_timers_call(calls, expected_timers)
62
+ assert_systemctl_call_starts_with(
63
+ calls,
64
+ 0,
65
+ SYSTEMCTL_USER_PREFIX + ["list-timers", "--all"],
66
+ includes: expected_timers
67
+ )
68
+ end
69
+
70
+ def assert_status_unit_calls(calls, expected_timers)
71
+ status_calls = calls.drop(1).map(&:first)
72
+ assert_equal expected_timers.length, status_calls.length
73
+ assert_equal expected_timers, status_calls.map(&:last).sort
74
+ status_calls.each { |args| assert_equal SYSTEMCTL_USER_PREFIX + ["status", args.last], args }
75
+ end
76
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+ require_relative "support/cli_test_helpers"
5
+
6
+ class CLIValidateTest < Minitest::Test
7
+ include CLITestHelpers
8
+
9
+ DUP_HOURLY_SCHEDULE = <<~RUBY
10
+ # frozen_string_literal: true
11
+
12
+ every :hour do
13
+ command "echo a"
14
+ end
15
+
16
+ every :hour do
17
+ command "echo b"
18
+ end
19
+ RUBY
20
+
21
+ HOURLY_SCHEDULE = <<~RUBY
22
+ # frozen_string_literal: true
23
+
24
+ every :hour do
25
+ command "echo a"
26
+ end
27
+ RUBY
28
+
29
+ def test_validate_runs_systemd_analyze_calendar_for_each_unique_on_calendar
30
+ with_project_dir do
31
+ write_schedule(DUP_HOURLY_SCHEDULE)
32
+ status, out, err, calls = run_validate("--verbose")
33
+ assert_cli_success(status, err)
34
+ assert_includes out, "OK OnCalendar=hourly"
35
+ assert_equal 1, calls.length
36
+ assert_equal %w[systemd-analyze calendar hourly], calls.fetch(0).fetch(0)
37
+ end
38
+ end
39
+
40
+ def test_validate_prints_message_when_no_on_calendar_and_verbose
41
+ with_project_dir do
42
+ write_empty_schedule
43
+ status, out, err, calls = run_validate("--verbose")
44
+ assert_cli_success(status, err)
45
+ assert_includes out, "No OnCalendar= values found"
46
+ assert_equal [], calls
47
+ end
48
+ end
49
+
50
+ def test_validate_verify_runs_systemd_analyze_verify
51
+ with_project_dir do
52
+ write_schedule(HOURLY_SCHEDULE)
53
+ status, out, err, calls = run_validate("--verify", "--verbose")
54
+ assert_cli_success(status, err)
55
+ assert_includes out, "OK systemd-analyze --user verify"
56
+ assert_equal %w[systemd-analyze --user verify], calls.fetch(1).fetch(0).take(3)
57
+ end
58
+ end
59
+
60
+ def test_validate_returns_nonzero_when_systemd_analyze_calendar_fails
61
+ with_project_dir do
62
+ write_schedule(HOURLY_SCHEDULE)
63
+ status, _out, err, calls = run_validate(exitstatus: 1, stderr: "boom\n")
64
+ assert_equal 1, status
65
+ assert_includes err, "systemd-analyze failed"
66
+ assert_includes err, "boom"
67
+ assert_equal %w[systemd-analyze calendar hourly], calls.fetch(0).fetch(0)
68
+ end
69
+ end
70
+
71
+ private
72
+
73
+ def run_validate(*args, **kwargs)
74
+ run_cli_with_capture3_stub(["validate", *args], **kwargs)
75
+ end
76
+
77
+ def write_schedule(contents)
78
+ FileUtils.mkdir_p("config")
79
+ File.write(File.join("config", "schedule.rb"), contents)
80
+ end
81
+ end
@@ -61,7 +61,7 @@ class DomainModelTest < Minitest::Test
61
61
  assert_raises(Wheneverd::InvalidCommandError) { Wheneverd::Job::Command.new(command: " ") }
62
62
 
63
63
  error = assert_raises(Wheneverd::InvalidCommandError) { Wheneverd::Job::Command.new(command: 123) }
64
- assert_includes error.message, "String"
64
+ assert_includes error.message, "String or an Array"
65
65
  end
66
66
 
67
67
  def test_triggers_render_timer_lines
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+
5
+ class DSLContextShellTest < Minitest::Test
6
+ def test_shell_requires_every_block
7
+ ctx = Wheneverd::DSL::Context.new(path: "/tmp/config/schedule.rb")
8
+ error = assert_raises(Wheneverd::DSL::LoadError) { ctx.shell("echo hi") }
9
+ assert_includes error.message, "shell() must be called inside every() block"
10
+ end
11
+
12
+ def test_shell_requires_string_script
13
+ ctx = Wheneverd::DSL::Context.new(path: "/tmp/config/schedule.rb")
14
+ error = assert_raises(Wheneverd::DSL::LoadError) { ctx.every("1m") { shell(123) } }
15
+ assert_includes error.message, "shell() script must be a String"
16
+ end
17
+
18
+ def test_shell_rejects_empty_script
19
+ ctx = Wheneverd::DSL::Context.new(path: "/tmp/config/schedule.rb")
20
+ error = assert_raises(Wheneverd::DSL::LoadError) { ctx.every("1m") { shell(" ") } }
21
+ assert_includes error.message, "shell() script must not be empty"
22
+ end
23
+ end
@@ -45,6 +45,29 @@ class DSLLoaderIntervalAndDurationTest < Minitest::Test
45
45
  assert_equal ["echo hello"], entry.jobs.map(&:command)
46
46
  end
47
47
 
48
+ def test_loads_argv_command
49
+ schedule = load_schedule(<<~RUBY)
50
+ every "5m" do
51
+ command ["echo", "hello world"]
52
+ end
53
+ RUBY
54
+
55
+ job = schedule.entries.fetch(0).jobs.fetch(0)
56
+ assert_equal ["echo", "hello world"], job.argv
57
+ assert_equal "echo \"hello world\"", job.command
58
+ end
59
+
60
+ def test_loads_shell_helper
61
+ schedule = load_schedule(<<~RUBY)
62
+ every "5m" do
63
+ shell "echo hello | sed -e s/hello/hi/"
64
+ end
65
+ RUBY
66
+
67
+ job = schedule.entries.fetch(0).jobs.fetch(0)
68
+ assert_equal ["/bin/bash", "-lc", "echo hello | sed -e s/hello/hi/"], job.argv
69
+ end
70
+
48
71
  def test_loads_duration_with_at_as_calendar
49
72
  schedule = load_schedule(<<~RUBY)
50
73
  every 1.day, at: "4:30 am" do
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+
5
+ class JobCommandTest < Minitest::Test
6
+ def test_accepts_argv_and_formats_execstart
7
+ command = Wheneverd::Job::Command.new(command: ["echo", "hello world"])
8
+ assert_equal ["echo", "hello world"], command.argv
9
+ assert_equal "echo \"hello world\"", command.command
10
+ end
11
+
12
+ def test_accepts_empty_argv_argument
13
+ command = Wheneverd::Job::Command.new(command: ["printf", "%s", ""])
14
+ assert_equal "printf %s \"\"", command.command
15
+ end
16
+
17
+ def test_rejects_invalid_argv
18
+ assert_raises(Wheneverd::InvalidCommandError) { Wheneverd::Job::Command.new(command: []) }
19
+ assert_raises(Wheneverd::InvalidCommandError) { Wheneverd::Job::Command.new(command: [" "]) }
20
+ assert_raises(Wheneverd::InvalidCommandError) { Wheneverd::Job::Command.new(command: ["echo", 1]) }
21
+ end
22
+
23
+ def test_rejects_argv_with_newlines
24
+ error = assert_raises(Wheneverd::InvalidCommandError) do
25
+ Wheneverd::Job::Command.new(command: %W[echo hi\nthere])
26
+ end
27
+ assert_includes error.message, "must not include NUL or newlines"
28
+ end
29
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+
5
+ class SystemdAnalyzeTest < Minitest::Test
6
+ def with_capture3_stub(exitstatus:, stdout: "", stderr: "")
7
+ calls = []
8
+ Thread.current[:open3_capture3_stub] = {
9
+ calls: calls,
10
+ stdout: stdout,
11
+ stderr: stderr,
12
+ exitstatus: exitstatus
13
+ }
14
+ yield calls
15
+ ensure
16
+ Thread.current[:open3_capture3_stub] = nil
17
+ end
18
+
19
+ def test_calendar_builds_command_and_returns_output
20
+ with_capture3_stub(exitstatus: 0, stdout: "ok\n", stderr: "") do |calls|
21
+ out, err = Wheneverd::Systemd::Analyze.calendar("hourly")
22
+ assert_equal "ok\n", out
23
+ assert_equal "", err
24
+ assert_equal [%w[systemd-analyze calendar hourly], {}], calls.fetch(0)
25
+ end
26
+ end
27
+
28
+ def test_verify_builds_user_verify_command
29
+ with_capture3_stub(exitstatus: 0) do |calls|
30
+ Wheneverd::Systemd::Analyze.verify(["/tmp/a.timer", "/tmp/a.service"], user: true)
31
+ assert_equal(
32
+ [["systemd-analyze", "--user", "verify", "/tmp/a.timer", "/tmp/a.service"], {}],
33
+ calls.fetch(0)
34
+ )
35
+ end
36
+ end
37
+
38
+ def test_calendar_raises_systemd_analyze_error_on_failure_with_details
39
+ with_capture3_stub(exitstatus: 1, stdout: "oops\n", stderr: "nope\n") do
40
+ error = assert_raises(Wheneverd::Systemd::SystemdAnalyzeError) do
41
+ Wheneverd::Systemd::Analyze.calendar("hourly")
42
+ end
43
+ assert_includes error.message, "status: 1"
44
+ assert_includes error.message, "stdout: oops"
45
+ assert_includes error.message, "stderr: nope"
46
+ end
47
+ end
48
+
49
+ def test_calendar_raises_systemd_analyze_error_when_missing
50
+ error = assert_raises(Wheneverd::Systemd::SystemdAnalyzeError) do
51
+ Wheneverd::Systemd::Analyze.calendar("hourly", systemd_analyze: "/no/such/systemd-analyze")
52
+ end
53
+ assert_includes error.message, "systemd-analyze not found"
54
+ end
55
+ end
@@ -62,6 +62,12 @@ class SystemdRendererIntervalTest < Minitest::Test
62
62
  assert_includes service.contents, "Type=oneshot"
63
63
  assert_includes service.contents, "ExecStart=echo hello"
64
64
  end
65
+
66
+ def test_interval_service_contains_execstart_for_argv
67
+ entry = interval_entry(seconds: 60, command: ["echo", "hello world"])
68
+ service = service_for(entry)
69
+ assert_includes service.contents, "ExecStart=echo \"hello world\""
70
+ end
65
71
  end
66
72
 
67
73
  class SystemdRendererCalendarTest < Minitest::Test
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.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - bigcurl
@@ -50,11 +50,14 @@ files:
50
50
  - lib/wheneverd/cli/current.rb
51
51
  - lib/wheneverd/cli/deactivate.rb
52
52
  - lib/wheneverd/cli/delete.rb
53
+ - lib/wheneverd/cli/diff.rb
53
54
  - lib/wheneverd/cli/help.rb
54
55
  - lib/wheneverd/cli/init.rb
55
56
  - lib/wheneverd/cli/linger.rb
56
57
  - lib/wheneverd/cli/reload.rb
57
58
  - lib/wheneverd/cli/show.rb
59
+ - lib/wheneverd/cli/status.rb
60
+ - lib/wheneverd/cli/validate.rb
58
61
  - lib/wheneverd/cli/write.rb
59
62
  - lib/wheneverd/core_ext/numeric_duration.rb
60
63
  - lib/wheneverd/dsl/at_normalizer.rb
@@ -69,6 +72,7 @@ files:
69
72
  - lib/wheneverd/interval.rb
70
73
  - lib/wheneverd/job/command.rb
71
74
  - lib/wheneverd/schedule.rb
75
+ - lib/wheneverd/systemd/analyze.rb
72
76
  - lib/wheneverd/systemd/calendar_spec.rb
73
77
  - lib/wheneverd/systemd/cron_parser.rb
74
78
  - lib/wheneverd/systemd/errors.rb
@@ -87,17 +91,24 @@ files:
87
91
  - test/cli_activate_test.rb
88
92
  - test/cli_current_test.rb
89
93
  - test/cli_deactivate_test.rb
94
+ - test/cli_diff_test.rb
90
95
  - test/cli_end_to_end_test.rb
96
+ - test/cli_init_test.rb
91
97
  - test/cli_linger_test.rb
92
98
  - test/cli_reload_test.rb
99
+ - test/cli_status_test.rb
93
100
  - test/cli_systemctl_integration_test.rb
94
101
  - test/cli_systemd_analyze_test.rb
95
102
  - test/cli_test.rb
103
+ - test/cli_validate_test.rb
96
104
  - test/domain_model_test.rb
97
105
  - test/dsl_calendar_symbol_period_list_test.rb
106
+ - test/dsl_context_shell_test.rb
98
107
  - test/dsl_loader_test.rb
108
+ - test/job_command_test.rb
99
109
  - test/support/cli_subprocess_test_helpers.rb
100
110
  - test/support/cli_test_helpers.rb
111
+ - test/systemd_analyze_test.rb
101
112
  - test/systemd_calendar_spec_test.rb
102
113
  - test/systemd_cron_parser_test.rb
103
114
  - test/systemd_renderer_errors_test.rb
@@ -131,7 +142,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
131
142
  - !ruby/object:Gem::Version
132
143
  version: '0'
133
144
  requirements: []
134
- rubygems_version: 4.0.3
145
+ rubygems_version: 4.0.4
135
146
  specification_version: 4
136
147
  summary: Wheneverd is to systemd timers what whenever is to cron.
137
148
  test_files: []