wheneverd 0.2.0 → 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 +4 -4
- data/CHANGELOG.md +13 -1
- data/FEATURE_SUMMARY.md +14 -3
- data/Gemfile.lock +2 -2
- data/README.md +38 -16
- data/lib/wheneverd/cli/diff.rb +98 -0
- data/lib/wheneverd/cli/init.rb +5 -1
- data/lib/wheneverd/cli/status.rb +37 -0
- data/lib/wheneverd/cli/validate.rb +50 -0
- data/lib/wheneverd/cli.rb +6 -0
- data/lib/wheneverd/dsl/context.rb +53 -11
- data/lib/wheneverd/entry.rb +2 -4
- data/lib/wheneverd/job/command.rb +88 -8
- data/lib/wheneverd/systemd/analyze.rb +56 -0
- data/lib/wheneverd/systemd/errors.rb +3 -0
- data/lib/wheneverd/systemd/unit_namer.rb +1 -1
- data/lib/wheneverd/version.rb +1 -1
- data/lib/wheneverd.rb +1 -0
- data/test/cli_diff_test.rb +118 -0
- data/test/cli_init_test.rb +49 -0
- data/test/cli_status_test.rb +76 -0
- data/test/cli_validate_test.rb +81 -0
- data/test/domain_model_test.rb +1 -1
- data/test/dsl_context_shell_test.rb +23 -0
- data/test/dsl_loader_test.rb +23 -0
- data/test/job_command_test.rb +29 -0
- data/test/systemd_analyze_test.rb +55 -0
- data/test/systemd_renderer_test.rb +6 -0
- metadata +13 -2
|
@@ -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
|
data/lib/wheneverd/version.rb
CHANGED
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
|
data/test/domain_model_test.rb
CHANGED
|
@@ -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
|
data/test/dsl_loader_test.rb
CHANGED
|
@@ -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.
|
|
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.
|
|
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: []
|