faymora 0.1

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,107 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Faymora::Node do
4
+ describe '#initialize' do
5
+ it 'inherits from GlooX::Node and forces serializer to MessagePack' do
6
+ expect(described_class < GlooX::Node).to be true
7
+
8
+ n = described_class.new(url: 'u', serializer: :other, extra: 1)
9
+ expect(n).to be_a(GlooX::Node)
10
+ expect(n.opts[:serializer]).to be(MessagePack)
11
+ expect(n.opts[:url]).to eq('u')
12
+ expect(n.opts[:extra]).to eq(1)
13
+ end
14
+ end
15
+
16
+ describe '.connect' do
17
+ it 'builds a Faymora::Client with handler: :node merged into options' do
18
+ client = instance_double(Faymora::Client)
19
+ expect(Faymora::Client).to receive(:new)
20
+ .with(hash_including(host: 'h', handler: :node))
21
+ .and_return(client)
22
+
23
+ result = described_class.connect(host: 'h')
24
+ expect(result).to eq(client)
25
+ end
26
+ end
27
+
28
+ describe '.start' do
29
+ it 'instantiates a node, calls #start and returns the instance' do
30
+ instance = instance_double(described_class)
31
+ expect(described_class).to receive(:new).with(foo: 1).and_return(instance)
32
+ expect(instance).to receive(:start)
33
+
34
+ expect(described_class.start(foo: 1)).to eq(instance)
35
+ end
36
+ end
37
+
38
+ describe '.boot' do
39
+ it 'calls .start and then .connect using the started node url' do
40
+ started = instance_double(described_class, url: 'xmpp://example')
41
+ expect(described_class).to receive(:start).with(role: 'worker').and_return(started)
42
+
43
+ client = instance_double(Faymora::Client)
44
+ expect(Faymora::Client).to receive(:new)
45
+ .with(hash_including(url: 'xmpp://example', handler: :node))
46
+ .and_return(client)
47
+
48
+ expect(described_class.boot(role: 'worker')).to eq(client)
49
+ end
50
+ end
51
+
52
+ describe '.when_ready' do
53
+ # A tiny fake reactor to drive the polling loop deterministically
54
+ class FakeReactor
55
+ def initialize
56
+ @calls = 0
57
+ end
58
+
59
+ def run_in_thread; end
60
+
61
+ # Call the provided block immediately, passing the block itself as `task`
62
+ def delay(_seconds, &blk)
63
+ blk.call(blk)
64
+ end
65
+
66
+ # Expose a toggling state through the same instance
67
+ def rpc_exception?
68
+ @calls += 1
69
+ @calls == 1
70
+ end
71
+ end
72
+
73
+ it 'polls client.alive? until success, then closes client and yields' do
74
+ # Replace Raktr constant with our fake reactor
75
+ stub_const('Raktr', Class.new do
76
+ def initialize; end
77
+ def run_in_thread; end
78
+ def delay(_s)
79
+ # Provide a task proc to the block and let the block control rescheduling
80
+ yield(proc {})
81
+ end
82
+ end)
83
+
84
+ reactor = double('reactor', run_in_thread: true)
85
+ allow(Raktr).to receive(:new).and_return(reactor)
86
+
87
+ # The `alive?` callback receives an object responding to `rpc_exception?` and `delay`.
88
+ poller = FakeReactor.new
89
+
90
+ client = instance_double(Faymora::Client)
91
+ expect(Faymora::Client).to receive(:new)
92
+ .with(hash_including(url: 'xmpp://n', handler: :node, client_max_retries: 0, connection_pool_size: 1))
93
+ .and_return(client)
94
+
95
+ # Simulate two polls: first with exception, then success
96
+ expect(client).to receive(:alive?) do |&blk|
97
+ blk.call(poller)
98
+ end.twice
99
+
100
+ expect(client).to receive(:close)
101
+
102
+ yielded = false
103
+ described_class.when_ready('xmpp://n') { yielded = true }
104
+ expect(yielded).to be true
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Faymora::Provision do
4
+ # Host class to include the module under test
5
+ class HostForProvision
6
+ include Faymora::Provision
7
+ end
8
+
9
+ describe 'inclusion' do
10
+ it 'adds a .provision class method to the host' do
11
+ expect(HostForProvision).to respond_to(:provision)
12
+ end
13
+ end
14
+
15
+ describe '.provision' do
16
+ it 'delegates to Slotz::Reservation.provision with the host class and options' do
17
+ options = { size: 3, region: 'eu' }
18
+ expect(Slotz::Reservation).to receive(:provision)
19
+ .with(HostForProvision, options)
20
+ .and_return(:token)
21
+
22
+ expect(HostForProvision.provision(options)).to eq(:token)
23
+ end
24
+
25
+ it 'passes an empty hash when options are omitted' do
26
+ expect(Slotz::Reservation).to receive(:provision)
27
+ .with(HostForProvision, {})
28
+ .and_return(:ok)
29
+
30
+ expect(HostForProvision.provision).to eq(:ok)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,43 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Faymora::RPC do
4
+ # Minimal host including the module under test
5
+ class DummyRPC
6
+ include Faymora::RPC
7
+
8
+ def initialize(url)
9
+ @url = url
10
+ end
11
+
12
+ def url
13
+ @url
14
+ end
15
+ end
16
+
17
+ let(:url) { 'xmpp://example' }
18
+ let(:host) { DummyRPC.new(url) }
19
+
20
+ describe '#client' do
21
+ it 'builds a Faymora::Client with the instance url and handler: :instance' do
22
+ client = instance_double(Faymora::Client)
23
+ expect(Faymora::Client).to receive(:new)
24
+ .with(hash_including(url: url, handler: :instance))
25
+ .and_return(client)
26
+
27
+ expect(host.client).to be(client)
28
+ end
29
+ end
30
+
31
+ describe '#server' do
32
+ it 'returns self' do
33
+ expect(host.server).to be(host)
34
+ end
35
+ end
36
+
37
+ describe '#node' do
38
+ it 'is defined (abstract) and returns nil by default' do
39
+ expect(host).to respond_to(:node)
40
+ expect(host.node).to be_nil
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,102 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Faymora::Service do
4
+ # Dummy host including the module under test
5
+ class DummyService
6
+ include Faymora::Service
7
+
8
+ # Provide a minimal client to satisfy Link expectations if ever invoked
9
+ def client
10
+ :client
11
+ end
12
+ end
13
+
14
+ let(:service_class) { Class.new(DummyService) }
15
+ let(:service) { service_class.new }
16
+
17
+ describe 'class DSL' do
18
+ it 'defaults would_interest? to true when no predicate set' do
19
+ c = Class.new(DummyService)
20
+ expect(c.would_interest?(Faymora::Item.new(foo: 1))).to be true
21
+ end
22
+
23
+ it 'allows setting an interest predicate via .interested_in' do
24
+ service_class.interested_in { |item| item.respond_to?(:foo) && item.foo == 2 }
25
+
26
+ expect(service_class.would_interest?(Faymora::Item.new(foo: 2))).to be true
27
+ expect(service_class.would_interest?(Faymora::Item.new(foo: 1))).to be false
28
+ end
29
+
30
+ it 'stores a consumer via .upon and exposes it via ._upon' do
31
+ blk = proc { |i| [:handled, i] }
32
+ service_class.upon(&blk)
33
+
34
+ stored = service_class._upon
35
+ expect(stored).to be_a(Proc)
36
+ item = Faymora::Item.new
37
+ expect(stored.call(item)).to eq([:handled, item])
38
+ end
39
+
40
+ it 'toggles ignore state via .ignore! and .ignore?' do
41
+ expect(service_class.ignore?).to be false
42
+ service_class.ignore!
43
+ expect(service_class.ignore?).to be true
44
+ end
45
+ end
46
+
47
+ describe '#join' do
48
+ it 'yields to the provided block and returns its result' do
49
+ yielded = nil
50
+ result = service.join { yielded = :ok; 123 }
51
+ expect(yielded).to eq(:ok)
52
+ expect(result).to eq(123)
53
+ end
54
+ end
55
+
56
+ describe '#interested?' do
57
+ it 'delegates to the class-level would_interest? with the item' do
58
+ service_class.interested_in { |i| i.foo == 'y' }
59
+ yes = Faymora::Item.new(foo: 'y')
60
+ no = Faymora::Item.new(foo: 'n')
61
+
62
+ expect(service.interested?(yes)).to be true
63
+ expect(service.interested?(no)).to be false
64
+ end
65
+ end
66
+
67
+ describe '#interested!' do
68
+ it 'marks the item as interesting' do
69
+ item = Faymora::Item.new
70
+ service.interested!(item)
71
+ expect(item.interesting).to be true
72
+ end
73
+ end
74
+
75
+ describe '#consume' do
76
+ it 'invokes the class-level consumer with the item and returns its result' do
77
+ service_class.upon { |i| i.processed = true; :done }
78
+ item = Faymora::Item.new
79
+ expect(service.consume(item)).to eq(:done)
80
+ expect(item.processed).to be true
81
+ end
82
+ end
83
+
84
+ describe '#process' do
85
+ it 'converts non-Item data via Item.from_data and consumes when interested' do
86
+ # Predicate always true
87
+ service_class.interested_in { |_i| true }
88
+ service_class.upon { |i| [i.class, i.foo] }
89
+
90
+ result = service.process({ foo: 5 })
91
+ expect(result).to eq([Faymora::Item, 5])
92
+ end
93
+
94
+ it 'returns nil and does not consume when not interested' do
95
+ service_class.interested_in { |_i| false }
96
+ expect(service_class).not_to receive(:_upon)
97
+
98
+ res = service.process(Faymora::Item.new(foo: 1))
99
+ expect(res).to be_nil
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,125 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Faymora::Throughput do
4
+ # Minimal host including the module under test
5
+ class DummyThroughput
6
+ include Faymora::Throughput
7
+
8
+ attr_accessor :input
9
+
10
+ def initialize
11
+ super
12
+ @clock_counter = 0
13
+ end
14
+
15
+ def has_input?
16
+ !!@input
17
+ end
18
+
19
+ # Expose counters for assertions in specs
20
+ def click_counter
21
+ @click_counter
22
+ end
23
+
24
+ def clock_counter
25
+ @clock_counter
26
+ end
27
+ end
28
+
29
+ let(:host) { DummyThroughput.new }
30
+
31
+ before do
32
+ # Avoid running real GC during specs
33
+ allow(GC).to receive(:start)
34
+ end
35
+
36
+ describe '#clock' do
37
+ it 'yields the item, appends a pass with class name, and always calls click!' do
38
+ item = Faymora::Item.new(passes: [])
39
+
40
+ # Verify it yields and records a pass
41
+ yielded = nil
42
+ expect(host).to receive(:click!).and_call_original
43
+
44
+ host.clock(item) { |i| yielded = i }
45
+
46
+ expect(yielded).to be(item)
47
+ expect(item.passes).not_to be_empty
48
+ last = item.passes.last
49
+ expect(last).to be_a(Faymora::Item::Pass)
50
+ expect(last.path).to eq(DummyThroughput.to_s)
51
+ end
52
+
53
+ it 'still calls click! when the block raises (ensure)' do
54
+ item = Faymora::Item.new(passes: [])
55
+ expect(host).to receive(:click!).and_call_original
56
+
57
+ expect do
58
+ host.clock(item) { raise 'boom' }
59
+ end.to raise_error(RuntimeError, 'boom')
60
+ end
61
+ end
62
+
63
+ describe '#click!' do
64
+ it 'increments the click counter and triggers GC' do
65
+ expect(host.click_counter).to eq 0
66
+ host.click!
67
+ expect(host.click_counter).to eq(1)
68
+ expect(GC).to have_received(:start)
69
+ end
70
+
71
+ it 'calls rectify when bottleneck? is true and not otherwise', focus: true do
72
+ allow(host).to receive(:bottleneck?).and_return(true)
73
+ expect(host).to receive(:rectify)
74
+ host.click!
75
+
76
+ allow(host).to receive(:bottleneck?).and_return(false)
77
+ expect(host).not_to receive(:rectify)
78
+ host.click!
79
+ end
80
+ end
81
+
82
+ describe '#throughput' do
83
+ it 'returns nil until warmed up, then clicks per clock_counter' do
84
+ # Not warmed up initially
85
+ expect(host.throughput).to be_nil
86
+
87
+ # Manually set counters to simulate warm state
88
+ host.instance_variable_set(:@clock_counter, 10)
89
+ host.instance_variable_set(:@click_counter, described_class::WARMED_UP_CLICKS)
90
+
91
+ t = host.throughput
92
+ expect(t).not_to be_nil
93
+ expect(t).to eq(described_class::WARMED_UP_CLICKS / 10.0)
94
+ end
95
+ end
96
+
97
+ describe '#bottleneck?' do
98
+ it 'is false when there is no input or throughput is nil' do
99
+ # No input
100
+ allow(host).to receive(:throughput).and_return(100.0)
101
+ expect(host.bottleneck?).to be false
102
+
103
+ # With input but no throughput
104
+ host.input = instance_double('Upstream', throughput: 200.0)
105
+ allow(host).to receive(:throughput).and_return(nil)
106
+ expect(host.bottleneck?).to be false
107
+ end
108
+
109
+ it 'returns true when input throughput is higher than own throughput' do
110
+ host.input = instance_double('Upstream', throughput: 10.0)
111
+ allow(host).to receive(:throughput).and_return(5.0)
112
+ expect(host.bottleneck?).to be true
113
+ end
114
+
115
+ it 'returns false when input throughput is lower or equal' do
116
+ host.input = instance_double('Upstream', throughput: 5.0)
117
+ allow(host).to receive(:throughput).and_return(10.0)
118
+ expect(host.bottleneck?).to be false
119
+
120
+ host.input = instance_double('Upstream', throughput: 10.0)
121
+ allow(host).to receive(:throughput).and_return(10.0)
122
+ expect(host.bottleneck?).to be false
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Ensure our support files (including a stub for `gloox`) are available to `require`.
4
+ $LOAD_PATH.unshift File.expand_path('support', __dir__)
5
+
6
+ # Provide a minimal `symbolize_keys` to match expectations in the library if it's not available.
7
+ unless {}.respond_to?(:symbolize_keys)
8
+ class Hash
9
+ def symbolize_keys
10
+ each_with_object({}) do |(k, v), h|
11
+ h[(k.is_a?(String) ? k.to_sym : k)] = v
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ require 'rspec'
18
+ require 'faymora'
19
+
20
+ RSpec.configure do |config|
21
+ config.expect_with :rspec do |expectations|
22
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
23
+ end
24
+
25
+ config.mock_with :rspec do |mocks|
26
+ mocks.verify_partial_doubles = true
27
+ end
28
+
29
+ config.shared_context_metadata_behavior = :apply_to_host_groups
30
+ end
@@ -0,0 +1,21 @@
1
+ # Minimal stub for `require 'gloox/node'` used by Faymora::Node
2
+ module GlooX
3
+ class Node
4
+ attr_reader :opts
5
+
6
+ def initialize(options = {})
7
+ # Mirror the `@opts` behavior used across the library
8
+ @opts = options.dup
9
+ end
10
+
11
+ # Provide a no-op start to satisfy Faymora::Node.start
12
+ def start
13
+ self
14
+ end
15
+
16
+ # Provide a basic url accessor used by .boot in specs (when not stubbed)
17
+ def url
18
+ @opts[:url]
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,12 @@
1
+ # Minimal stub for the external `gloox` gem used by the library during specs.
2
+ module GlooX
3
+ class Client
4
+ attr_reader :opts
5
+
6
+ def initialize(options = {})
7
+ # Store a dup so mutation outside won’t affect internals, and keep the
8
+ # exact semantics the library relies on: the ivar name is `@opts`.
9
+ @opts = options.dup
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,10 @@
1
+ # Minimal stub for the external `slotz` gem used by the Provision module in specs.
2
+ module Slotz
3
+ module Reservation
4
+ # Placeholder to be stubbed in specs. Keeping a default no-op implementation
5
+ # to avoid unexpected NameErrors when not explicitly stubbed.
6
+ def self.provision(_klass, _options = {})
7
+ :ok
8
+ end
9
+ end
10
+ end
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: faymora
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Tasos Laskos
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2026-01-06 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: gloox
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: ostruct
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: logger
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ description: ''
55
+ email: tasos.laskos@gmail.com
56
+ executables: []
57
+ extensions: []
58
+ extra_rdoc_files:
59
+ - LICENSE.md
60
+ - README.md
61
+ files:
62
+ - Gemfile
63
+ - LICENSE.md
64
+ - README.md
65
+ - faymora.gemspec
66
+ - lib/faymora.rb
67
+ - lib/faymora/client.rb
68
+ - lib/faymora/connection.rb
69
+ - lib/faymora/instance.rb
70
+ - lib/faymora/item.rb
71
+ - lib/faymora/link.rb
72
+ - lib/faymora/node.rb
73
+ - lib/faymora/provision.rb
74
+ - lib/faymora/rectifier.rb
75
+ - lib/faymora/rpc.rb
76
+ - lib/faymora/service.rb
77
+ - lib/faymora/throughput.rb
78
+ - lib/faymora/version.rb
79
+ - spec/faymora/client_spec.rb
80
+ - spec/faymora/connection_spec.rb
81
+ - spec/faymora/item_spec.rb
82
+ - spec/faymora/link_spec.rb
83
+ - spec/faymora/node_spec.rb
84
+ - spec/faymora/provision_spec.rb
85
+ - spec/faymora/rpc_spec.rb
86
+ - spec/faymora/service_spec.rb
87
+ - spec/faymora/throughput_spec.rb
88
+ - spec/spec_helper.rb
89
+ - spec/support/gloox.rb
90
+ - spec/support/gloox/node.rb
91
+ - spec/support/slotz.rb
92
+ homepage: https://github.com/faymora/faymora
93
+ licenses:
94
+ - MPL v2
95
+ metadata: {}
96
+ rdoc_options:
97
+ - "--charset=UTF-8"
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ required_rubygems_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ requirements: []
111
+ rubygems_version: 3.6.9
112
+ specification_version: 4
113
+ summary: ''
114
+ test_files:
115
+ - spec/faymora/client_spec.rb
116
+ - spec/faymora/connection_spec.rb
117
+ - spec/faymora/item_spec.rb
118
+ - spec/faymora/link_spec.rb
119
+ - spec/faymora/node_spec.rb
120
+ - spec/faymora/provision_spec.rb
121
+ - spec/faymora/rpc_spec.rb
122
+ - spec/faymora/service_spec.rb
123
+ - spec/faymora/throughput_spec.rb
124
+ - spec/spec_helper.rb
125
+ - spec/support/gloox.rb
126
+ - spec/support/gloox/node.rb
127
+ - spec/support/slotz.rb