rspec-openhab-scripting 1.0.0

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