rspec-openhab-scripting 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|