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.
@@ -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