rspec-openhab-scripting 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/rspec/openhab/actions.rb +20 -0
- data/lib/rspec/openhab/api.rb +62 -0
- data/lib/rspec/openhab/core/item_proxy.rb +19 -0
- data/lib/rspec/openhab/core/logger.rb +43 -0
- data/lib/rspec/openhab/core/mocks/bundle_install_support.rb +26 -0
- data/lib/rspec/openhab/core/mocks/bundle_resolver.rb +32 -0
- data/lib/rspec/openhab/core/mocks/event_admin.rb +148 -0
- data/lib/rspec/openhab/core/mocks/persistence_service.rb +144 -0
- data/lib/rspec/openhab/core/mocks/synchronous_executor.rb +56 -0
- data/lib/rspec/openhab/core/mocks/thing_handler.rb +77 -0
- data/lib/rspec/openhab/core/openhab_setup.rb +11 -0
- data/lib/rspec/openhab/dsl/rules/triggers/watch.rb +11 -0
- data/lib/rspec/openhab/dsl/timers/timer.rb +88 -0
- data/lib/rspec/openhab/helpers.rb +249 -0
- data/lib/rspec/openhab/hooks.rb +56 -0
- data/lib/rspec/openhab/jruby.rb +45 -0
- data/lib/rspec/openhab/karaf.rb +733 -0
- data/lib/rspec/openhab/shell.rb +30 -0
- data/lib/rspec/openhab/suspend_rules.rb +56 -0
- data/lib/rspec/openhab/version.rb +7 -0
- data/lib/rspec-openhab-scripting.rb +17 -0
- metadata +232 -0
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "timecop"
|
4
|
+
|
5
|
+
module OpenHAB
|
6
|
+
module DSL
|
7
|
+
class Timer
|
8
|
+
module MockedZonedDateTime
|
9
|
+
def now
|
10
|
+
mocked_time_stack_item = Timecop.top_stack_item
|
11
|
+
return super unless mocked_time_stack_item
|
12
|
+
|
13
|
+
instant = java.time.Instant.of_epoch_milli((Time.now.to_f * 1000).to_i)
|
14
|
+
ZonedDateTime.of_instant(instant, java.time.ZoneId.system_default)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
ZonedDateTime.singleton_class.prepend(MockedZonedDateTime)
|
18
|
+
|
19
|
+
# extend Timecop to support java time classes
|
20
|
+
module TimeCopStackItem
|
21
|
+
def parse_time(*args)
|
22
|
+
if args.length == 1 && args.first.is_a?(Duration)
|
23
|
+
return time_klass.at(ZonedDateTime.now.plus(args.first).to_instant.to_epoch_milli / 1000.0)
|
24
|
+
end
|
25
|
+
|
26
|
+
super
|
27
|
+
end
|
28
|
+
end
|
29
|
+
Timecop::TimeStackItem.prepend(TimeCopStackItem)
|
30
|
+
|
31
|
+
attr_reader :execution_time
|
32
|
+
|
33
|
+
def initialize(duration:, thread_locals: {}, &block) # rubocop:disable Lint/UnusedMethodArgument
|
34
|
+
@block = block
|
35
|
+
reschedule(duration)
|
36
|
+
end
|
37
|
+
|
38
|
+
def reschedule(duration = nil)
|
39
|
+
@duration = duration || @duration
|
40
|
+
@execution_time = ::OpenHAB::DSL.to_zdt(@duration)
|
41
|
+
@executed = @cancelled = false
|
42
|
+
|
43
|
+
Timers.timer_manager.add(self)
|
44
|
+
end
|
45
|
+
|
46
|
+
def execute
|
47
|
+
raise "Timer already cancelled" if cancelled?
|
48
|
+
raise "Timer already executed" if terminated?
|
49
|
+
|
50
|
+
@block.call(self)
|
51
|
+
Timers.timer_manager.delete(self)
|
52
|
+
@executed = true
|
53
|
+
end
|
54
|
+
|
55
|
+
def cancel
|
56
|
+
Timers.timer_manager.delete(self)
|
57
|
+
@executed = false
|
58
|
+
@cancelled = true
|
59
|
+
true
|
60
|
+
end
|
61
|
+
|
62
|
+
def cancelled?
|
63
|
+
@cancelled
|
64
|
+
end
|
65
|
+
|
66
|
+
def terminated?
|
67
|
+
@executed || @cancelled
|
68
|
+
end
|
69
|
+
|
70
|
+
def running?
|
71
|
+
active? && @execution_time > ZonedDateTime.now
|
72
|
+
end
|
73
|
+
|
74
|
+
def active?
|
75
|
+
!terminated?
|
76
|
+
end
|
77
|
+
alias_method :is_active, :active?
|
78
|
+
end
|
79
|
+
|
80
|
+
module Support
|
81
|
+
class TimerManager
|
82
|
+
def execute_timers
|
83
|
+
@timers.each { |t| t.execute if t.active? && t.execution_time < ZonedDateTime.now }
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,249 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OpenHAB
|
4
|
+
module Transform
|
5
|
+
class << self
|
6
|
+
def add_script(modules, script)
|
7
|
+
full_name = modules.join("/")
|
8
|
+
name = modules.pop
|
9
|
+
(@scripts ||= {})[full_name] = engine_factory.script_engine.compile(script)
|
10
|
+
|
11
|
+
mod = modules.inject(self) { |m, n| m.const_get(n, false) }
|
12
|
+
mod.singleton_class.define_method(name) do |input, **kwargs|
|
13
|
+
Transform.send(:transform, full_name, input, kwargs)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def engine_factory
|
20
|
+
@engine_factory ||= org.jruby.embed.jsr223.JRubyEngineFactory.new
|
21
|
+
end
|
22
|
+
|
23
|
+
def transform(name, input, kwargs)
|
24
|
+
script = @scripts[name]
|
25
|
+
ctx = script.engine.context
|
26
|
+
ctx.set_attribute("input", input.to_s, javax.script.ScriptContext::ENGINE_SCOPE)
|
27
|
+
kwargs.each do |(k, v)|
|
28
|
+
ctx.set_attribute(k.to_s, v.to_s, javax.script.ScriptContext::ENGINE_SCOPE)
|
29
|
+
end
|
30
|
+
script.eval
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
module RSpec
|
37
|
+
module OpenHAB
|
38
|
+
module Helpers
|
39
|
+
module BindingHelper
|
40
|
+
def add_kwargs_to_current_binding(binding, kwargs)
|
41
|
+
kwargs.each { |(k, v)| binding.local_variable_set(k, v) }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private_constant :BindingHelper
|
46
|
+
|
47
|
+
singleton_class.include(Helpers)
|
48
|
+
|
49
|
+
def autoupdate_all_items
|
50
|
+
if instance_variable_defined?(:@autoupdated_items)
|
51
|
+
raise RuntimeError "you should only call `autoupdate_all_items` once per spec"
|
52
|
+
end
|
53
|
+
|
54
|
+
@autoupdated_items = []
|
55
|
+
|
56
|
+
$ir.for_each do |_provider, item|
|
57
|
+
if item.meta.key?("autoupdate")
|
58
|
+
@autoupdated_items << item.meta.delete("autoupdate")
|
59
|
+
item.meta["autoupdate"] = true
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def execute_timers
|
65
|
+
::OpenHAB::DSL::Timers.timer_manager.execute_timers
|
66
|
+
end
|
67
|
+
|
68
|
+
def suspend_rules(&block)
|
69
|
+
SuspendRules.suspend_rules(&block)
|
70
|
+
end
|
71
|
+
|
72
|
+
def trigger_rule(rule_name, event = nil)
|
73
|
+
@rules ||= ::OpenHAB::DSL::Rules::Rule.script_rules.each_with_object({}) { |r, obj| obj[r.name] = r }
|
74
|
+
|
75
|
+
@rules.fetch(rule_name).execute(nil, { "event" => event })
|
76
|
+
end
|
77
|
+
|
78
|
+
def trigger_channel(channel, event)
|
79
|
+
channel = org.openhab.core.thing.ChannelUID.new(channel) if channel.is_a?(String)
|
80
|
+
channel = channel.uid if channel.is_a?(org.openhab.core.thing.Channel)
|
81
|
+
thing = channel.thing
|
82
|
+
thing.handler.callback.channel_triggered(nil, channel, event)
|
83
|
+
end
|
84
|
+
|
85
|
+
def autorequires
|
86
|
+
requires = jrubyscripting_config&.get("require") || ""
|
87
|
+
requires.split(",").each do |f|
|
88
|
+
require f.strip
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def launch_karaf(include_bindings: true,
|
93
|
+
include_jsondb: true,
|
94
|
+
private_confdir: false,
|
95
|
+
use_root_instance: false)
|
96
|
+
karaf = RSpec::OpenHAB::Karaf.new("#{Dir.pwd}/.karaf")
|
97
|
+
karaf.include_bindings = include_bindings
|
98
|
+
karaf.include_jsondb = include_jsondb
|
99
|
+
karaf.private_confdir = private_confdir
|
100
|
+
karaf.use_root_instance = use_root_instance
|
101
|
+
main = karaf.launch
|
102
|
+
|
103
|
+
ENV["RUBYLIB"] ||= ""
|
104
|
+
ENV["RUBYLIB"] += ":" unless ENV["RUBYLIB"].empty?
|
105
|
+
ENV["RUBYLIB"] += rubylib_dir
|
106
|
+
require "openhab"
|
107
|
+
require "rspec/openhab/core/logger"
|
108
|
+
|
109
|
+
require "rspec/openhab/core/mocks/persistence_service"
|
110
|
+
|
111
|
+
# override several openhab-scripting methods
|
112
|
+
require_relative "actions"
|
113
|
+
require_relative "core/item_proxy"
|
114
|
+
require_relative "dsl/timers/timer"
|
115
|
+
# TODO: still needed?
|
116
|
+
require_relative "dsl/rules/triggers/watch"
|
117
|
+
|
118
|
+
ps = RSpec::OpenHAB::Core::Mocks::PersistenceService.instance
|
119
|
+
bundle = org.osgi.framework.FrameworkUtil.get_bundle(org.openhab.core.persistence.PersistenceService)
|
120
|
+
bundle.bundle_context.register_service(org.openhab.core.persistence.PersistenceService.java_class, ps, nil)
|
121
|
+
|
122
|
+
# wait for the rule engine
|
123
|
+
rs = ::OpenHAB::Core::OSGI.service("org.openhab.core.service.ReadyService")
|
124
|
+
filter = org.openhab.core.service.ReadyMarkerFilter.new
|
125
|
+
.with_type(org.openhab.core.service.StartLevelService::STARTLEVEL_MARKER_TYPE)
|
126
|
+
.with_identifier(org.openhab.core.service.StartLevelService::STARTLEVEL_RULEENGINE.to_s)
|
127
|
+
|
128
|
+
karaf.send(:wait) do |continue|
|
129
|
+
rs.register_tracker(org.openhab.core.service.ReadyService::ReadyTracker.impl { continue.call }, filter)
|
130
|
+
end
|
131
|
+
|
132
|
+
# RSpec additions
|
133
|
+
require "rspec/openhab/suspend_rules"
|
134
|
+
|
135
|
+
if ::RSpec.respond_to?(:config)
|
136
|
+
::RSpec.configure do |config|
|
137
|
+
config.include OpenHAB::Core::EntityLookup
|
138
|
+
end
|
139
|
+
end
|
140
|
+
main
|
141
|
+
rescue Exception => e
|
142
|
+
puts e.inspect
|
143
|
+
puts e.backtrace
|
144
|
+
raise
|
145
|
+
end
|
146
|
+
|
147
|
+
def load_rules
|
148
|
+
automation_path = "#{org.openhab.core.OpenHAB.config_folder}/automation/jsr223/ruby/personal"
|
149
|
+
|
150
|
+
RSpec::OpenHAB::SuspendRules.suspend_rules do
|
151
|
+
Dir["#{automation_path}/*.rb"].each do |f|
|
152
|
+
load f
|
153
|
+
rescue Exception => e
|
154
|
+
warn "Failed loading #{f}: #{e.inspect}"
|
155
|
+
warn e.backtrace
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def load_transforms
|
161
|
+
transform_path = "#{org.openhab.core.OpenHAB.config_folder}/transform"
|
162
|
+
Dir["#{transform_path}/**/*.script"].each do |filename|
|
163
|
+
script = File.read(filename)
|
164
|
+
next unless ruby_file?(script)
|
165
|
+
|
166
|
+
filename.slice!(0..transform_path.length)
|
167
|
+
dir = File.dirname(filename)
|
168
|
+
modules = dir == "." ? [] : moduleize(dir)
|
169
|
+
basename = File.basename(filename)
|
170
|
+
method = basename[0...-7]
|
171
|
+
modules << method
|
172
|
+
::OpenHAB::Transform.add_script(modules, script)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
private
|
177
|
+
|
178
|
+
def jrubyscripting_config
|
179
|
+
ca = ::OpenHAB::Core::OSGI.service("org.osgi.service.cm.ConfigurationAdmin")
|
180
|
+
ca.get_configuration("org.openhab.automation.jrubyscripting", nil)&.properties
|
181
|
+
end
|
182
|
+
|
183
|
+
def rubylib_dir
|
184
|
+
jrubyscripting_config&.get("rubylib") || "#{org.openhab.core.OpenHAB.config_folder}/automation/lib/ruby"
|
185
|
+
end
|
186
|
+
|
187
|
+
EMACS_MODELINE_REGEXP = /# -\*-(.+)-\*-/.freeze
|
188
|
+
|
189
|
+
def parse_emacs_modeline(line)
|
190
|
+
line[EMACS_MODELINE_REGEXP, 1]
|
191
|
+
&.split(";")
|
192
|
+
&.map(&:strip)
|
193
|
+
&.map { |l| l.split(":", 2).map(&:strip).tap { |a| a[1] ||= nil } }
|
194
|
+
&.to_h
|
195
|
+
end
|
196
|
+
|
197
|
+
def ruby_file?(script)
|
198
|
+
# check the first 1KB for an emacs magic comment
|
199
|
+
script[0..1024].split("\n").any? { |line| parse_emacs_modeline(line)&.dig("mode") == "ruby" }
|
200
|
+
end
|
201
|
+
|
202
|
+
def moduleize(term)
|
203
|
+
term
|
204
|
+
.sub(/^[a-z\d]*/, &:capitalize)
|
205
|
+
.gsub(%r{(?:_|(/))([a-z\d]*)}) { "#{$1}#{$2.capitalize}" }
|
206
|
+
.split("/")
|
207
|
+
end
|
208
|
+
|
209
|
+
# need to transfer autoupdate metadata from GenericMetadataProvider to ManagedMetadataProvider
|
210
|
+
# so that we can mutate it in the future
|
211
|
+
def set_up_autoupdates
|
212
|
+
gmp = ::OpenHAB::Core::OSGI.service("org.openhab.core.model.item.internal.GenericMetadataProvider")
|
213
|
+
mr = ::OpenHAB::Core::OSGI.service("org.openhab.core.items.MetadataRegistry")
|
214
|
+
mmp = mr.managed_provider.get
|
215
|
+
to_add = []
|
216
|
+
gmp.all.each do |metadata|
|
217
|
+
next unless metadata.uid.namespace == "autoupdate"
|
218
|
+
|
219
|
+
to_add << metadata
|
220
|
+
end
|
221
|
+
gmp.remove_metadata_by_namespace("autoupdate")
|
222
|
+
|
223
|
+
to_add.each do |m|
|
224
|
+
if mmp.get(m.uid)
|
225
|
+
mmp.update(m)
|
226
|
+
else
|
227
|
+
mmp.add(m)
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
def restore_autoupdate_items
|
233
|
+
return unless instance_variable_defined?(:@autoupdated_items)
|
234
|
+
|
235
|
+
mr = ::OpenHAB::Core::OSGI.service("org.openhab.core.items.MetadataRegistry")
|
236
|
+
@autoupdated_items&.each do |meta|
|
237
|
+
mr.update(meta)
|
238
|
+
end
|
239
|
+
@autoupdated_items = nil
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
if RSpec.respond_to?(:configure)
|
244
|
+
RSpec.configure do |config|
|
245
|
+
config.include Helpers
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RSpec
|
4
|
+
module OpenHAB
|
5
|
+
Object.include RSpec::OpenHAB::Helpers if defined?(IRB)
|
6
|
+
|
7
|
+
if RSpec.respond_to?(:configure)
|
8
|
+
RSpec.configure do |config|
|
9
|
+
config.add_setting :include_openhab_bindings, default: true
|
10
|
+
config.add_setting :include_openhab_jsondb, default: true
|
11
|
+
config.add_setting :private_openhab_confdir, default: false
|
12
|
+
config.add_setting :use_root_openhab_instance, default: false
|
13
|
+
|
14
|
+
config.before(:suite) do
|
15
|
+
Helpers.launch_karaf(include_bindings: config.include_openhab_bindings,
|
16
|
+
include_jsondb: config.include_openhab_jsondb,
|
17
|
+
private_confdir: config.private_openhab_confdir,
|
18
|
+
use_root_instance: config.use_root_openhab_instance)
|
19
|
+
config.include ::OpenHAB::Core::EntityLookup
|
20
|
+
Helpers.autorequires unless config.private_openhab_confdir
|
21
|
+
Helpers.send(:set_up_autoupdates)
|
22
|
+
Helpers.load_transforms
|
23
|
+
Helpers.load_rules
|
24
|
+
end
|
25
|
+
|
26
|
+
config.before do
|
27
|
+
suspend_rules do
|
28
|
+
$ir.for_each do |_provider, item|
|
29
|
+
next if item.is_a?(GroupItem) # groups only have calculated states
|
30
|
+
|
31
|
+
item.state = NULL unless item.raw_state == NULL
|
32
|
+
end
|
33
|
+
end
|
34
|
+
@known_rules = ::OpenHAB::Core.rule_registry.all.map(&:uid)
|
35
|
+
end
|
36
|
+
|
37
|
+
config.before do
|
38
|
+
@item_provider = ::OpenHAB::DSL::Items::ItemProvider.send(:new)
|
39
|
+
allow(::OpenHAB::DSL::Items::ItemProvider).to receive(:instance).and_return(@item_provider)
|
40
|
+
end
|
41
|
+
|
42
|
+
config.after do
|
43
|
+
# remove rules created during the spec
|
44
|
+
(::OpenHAB::Core.rule_registry.all.map(&:uid) - @known_rules).each do |uid|
|
45
|
+
::OpenHAB::Core.rule_registry.remove(uid)
|
46
|
+
end
|
47
|
+
$ir.remove_provider(@item_provider) if @item_provider
|
48
|
+
::OpenHAB::DSL::Timers.timer_manager.cancel_all
|
49
|
+
Timecop.return
|
50
|
+
restore_autoupdate_items
|
51
|
+
Core::Mocks::PersistenceService.instance.reset
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RSpec
|
4
|
+
module OpenHAB
|
5
|
+
module JRuby
|
6
|
+
# Basically org.jruby.embed.osgi.OSGiIsolatedScriptingContainer$BundleGetResources,
|
7
|
+
# but implemented in Ruby so that it doesn't have a hard dependency on
|
8
|
+
# org.osgi.bundle.Bundle -- which we may need to load!
|
9
|
+
class OSGiBundleClassLoader
|
10
|
+
include org.jruby.util.Loader
|
11
|
+
|
12
|
+
def initialize(bundle)
|
13
|
+
@bundle = bundle
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_resource(path)
|
17
|
+
@bundle.get_resource(path)
|
18
|
+
end
|
19
|
+
|
20
|
+
def get_resources(path)
|
21
|
+
@bundle.get_resources(path)
|
22
|
+
end
|
23
|
+
|
24
|
+
def load_class(name)
|
25
|
+
@bundle.load_class(name)
|
26
|
+
end
|
27
|
+
|
28
|
+
def get_class_loader # rubocop:disable Naming/AccessorMethodName
|
29
|
+
@bundle&.adapt(org.osgi.framework.wiring.BundleWiring.java_class)&.class_loader
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
module InstanceConfig
|
34
|
+
def add_loader(loader)
|
35
|
+
# have to use Ruby-style class reference for the defined? check
|
36
|
+
if defined?(Java::OrgOsgiFramework::Bundle) && loader.is_a?(org.osgi.framework.Bundle)
|
37
|
+
loader = OSGiBundleClassLoader.new(loader)
|
38
|
+
end
|
39
|
+
super(loader)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
org.jruby.RubyInstanceConfig.prepend(InstanceConfig)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|