tools

various tools I have been using throughout the years
Log | Files | Refs | README | LICENSE

commit c688140c7294c82e20d234082e3c1703949947dd
parent 0b71de1ad9674a821afb5b2cd18b5a938735c296
Author: mtmn <miro@haravara.org>
Date:   Sun, 10 May 2026 18:40:56 +0200

hue: fix code smells, add tooling

Diffstat:
Mhue/.gitignore | 8++++++++
Ahue/.reek.yml | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ahue/.rspec | 2++
Ahue/.rubocop.yml | 105+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mhue/Gemfile | 21+++++++++++++++++++++
Mhue/Gemfile.lock | 231+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mhue/Rakefile | 21++++++++++++++++++++-
Dhue/hue-0.4.0.gem | 0
Mhue/hue.gemspec | 11++++-------
Mhue/lib/hue.rb | 96++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mhue/lib/hue/animations/sunrise.rb | 33++++++++++++++++++---------------
Mhue/lib/hue/bridge.rb | 57+++++++++++++++++++++++++++++++--------------------------
Mhue/lib/hue/cli.rb | 3++-
Mhue/lib/hue/cli/commands/delete.rb | 8++------
Mhue/lib/hue/cli/commands/light.rb | 12+++---------
Mhue/lib/hue/cli/processors/light_state_alias.rb | 9+++++----
Mhue/lib/hue/colors.rb | 9++-------
Mhue/lib/hue/colors/color.rb | 2+-
Mhue/lib/hue/colors/color_temperature.rb | 16++++++----------
Mhue/lib/hue/colors/hue_saturation.rb | 13+++++++------
Mhue/lib/hue/colors/rgb.rb | 19++++++++-----------
Mhue/lib/hue/colors/xy.rb | 6+++---
Mhue/lib/hue/config/abstract.rb | 7++++---
Mhue/lib/hue/config/application.rb | 6+++---
Mhue/lib/hue/config/bridge.rb | 4++--
Mhue/lib/hue/utilities/indentation.rb | 6++++--
Dhue/pkg/hue-0.1.0.gem | 0
Ahue/sig/.keep | 0
Ahue/sorbet/config | 7+++++++
Mhue/spec/cli_spec.rb | 4++--
Mhue/spec/hue/bridge_spec.rb | 36+++++++++++++++++-------------------
Mhue/spec/hue/bulb_spec.rb | 55++++++++++++++++++++++++++-----------------------------
Ahue/spec/hue/cli/command_spec.rb | 42++++++++++++++++++++++++++++++++++++++++++
Ahue/spec/hue/cli/commands/delete_spec.rb | 25+++++++++++++++++++++++++
Ahue/spec/hue/cli/commands/light_spec.rb | 65+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ahue/spec/hue/cli/commands/lights_spec.rb | 31+++++++++++++++++++++++++++++++
Ahue/spec/hue/cli_spec.rb | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mhue/spec/hue/colors/color_spec.rb | 16++++++++--------
Mhue/spec/hue/colors/color_temperature_spec.rb | 52+++++++++++++++++++++++++---------------------------
Mhue/spec/hue/colors/hue_saturation_spec.rb | 46++++++++++++++++++++++------------------------
Mhue/spec/hue/colors/rgb_spec.rb | 27+++++++++++++--------------
Mhue/spec/hue/colors/xy_spec.rb | 27+++++++++++++--------------
Mhue/spec/hue/colors_spec.rb | 37+++++++++++++++++++------------------
Mhue/spec/hue/config/abstract_spec.rb | 27++++++++++++++++-----------
Mhue/spec/hue/config/application_spec.rb | 34+++++++++++++++++-----------------
Mhue/spec/hue/config/bridge_spec.rb | 14+++++++-------
Ahue/spec/hue/extensions/bridge_spec.rb | 58++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ahue/spec/hue/extensions/bulb_spec.rb | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Ahue/spec/hue/utilities/indentation_spec.rb | 39+++++++++++++++++++++++++++++++++++++++
Mhue/spec/hue_spec.rb | 50++++++++++++++++++++++++--------------------------
Mhue/spec/spec_helper.rb | 11+++++++++++
Ahue/spec/test_helpers.rb | 150+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
52 files changed, 1353 insertions(+), 384 deletions(-)

diff --git a/hue/.gitignore b/hue/.gitignore @@ -1 +1,9 @@ vendor/ +coverage/ +pkg/ +tmp/ +.rubocop_cache/ +sorbet/rbi/gems/ +sorbet/rbi/dsl/ +sorbet/rbi/annotations/ +sorbet/tapioca/require.rb diff --git a/hue/.reek.yml b/hue/.reek.yml @@ -0,0 +1,71 @@ +--- +exclude_paths: + - spec + - pkg + - vendor + - sorbet + - coverage + +detectors: + IrresponsibleModule: + enabled: false + Attribute: + enabled: false + BooleanParameter: + enabled: false + DataClump: + enabled: false + MissingSafeMethod: + enabled: false + InstanceVariableAssumption: + enabled: false + TooManyMethods: + exclude: + - Hue::Bridge + - Hue::Bulb + TooManyInstanceVariables: + exclude: + - Hue::Bulb + TooManyConstants: + exclude: + - Hue::Colors::HueSaturation + - Hue + TooManyStatements: + max_statements: 12 + exclude: + - Hue::Colors::HueSaturation#to_rgb + - Hue::Colors::RGB#to_hash + UncommunicativeVariableName: + accept: + - _ + - i + - t + - h + - s + - v + - x + - y + - z + - p + - q + - r + - g + - b + - f + - d + - k + - m + - a + - e + UtilityFunction: + public_methods_only: true + FeatureEnvy: + exclude: + - Hue::Colors::ColorTemperature#to_rgb + - Hue::Colors::HueSaturation#to_rgb + - Hue::Colors::RGB#== + - Hue::Config::Abstract#== + - Hue::Bridge#print_lights + - Hue::Bridge#print_proxy_info + - Hue::Bridge#receive + - Hue::Bridge#send_request diff --git a/hue/.rspec b/hue/.rspec @@ -0,0 +1,2 @@ +--exclude-pattern "vendor/**/*_spec.rb,pkg/**/*_spec.rb" +--require spec_helper diff --git a/hue/.rubocop.yml b/hue/.rubocop.yml @@ -0,0 +1,105 @@ +plugins: + - rubocop-rake + - rubocop-rspec + +AllCops: + TargetRubyVersion: 3.0 + NewCops: enable + SuggestExtensions: false + Exclude: + - 'pkg/**/*' + - 'vendor/**/*' + - 'sorbet/**/*' + - 'tmp/**/*' + - 'coverage/**/*' + - 'hue-*.gem' + +Style/Documentation: + Enabled: false + +Style/StringLiterals: + EnforcedStyle: single_quotes + +Metrics/BlockLength: + Exclude: + - 'spec/**/*' + - 'Rakefile' + - '*.gemspec' + +Metrics/MethodLength: + Max: 20 + Exclude: + - 'lib/hue/colors/hue_saturation.rb' + - 'lib/hue/colors/rgb.rb' + +Layout/LineLength: + Max: 120 + +# Color math methods are inherently dense; the public CLI verbs Bulb#on/#off, +# Hue.remove_default look like predicates but aren't. +Metrics/AbcSize: + Exclude: + - 'lib/hue/animations/candle.rb' + - 'lib/hue/colors/color_temperature.rb' + - 'lib/hue/colors/hue_saturation.rb' + - 'lib/hue/colors/rgb.rb' + +Metrics/CyclomaticComplexity: + Exclude: + - 'lib/hue/colors/hue_saturation.rb' + +Metrics/ClassLength: + Exclude: + - 'lib/hue/bulb.rb' + +Metrics/ModuleLength: + Exclude: + - 'lib/hue.rb' + - 'spec/test_helpers.rb' + +Metrics/ParameterLists: + CountKeywordArgs: false + Max: 5 + MaxOptionalParameters: 4 + +Naming/PredicateMethod: + Enabled: false + +Style/MultilineBlockChain: + Enabled: false + +Style/OptionalBooleanParameter: + Exclude: + - 'spec/test_helpers.rb' + +# Specs verify behavior via webmock stubs and side effects; explicit expects +# are not always present, and the project keeps a few stylistic rules relaxed. +RSpec/ExampleLength: + Max: 15 + +RSpec/MultipleExpectations: + Max: 10 + +RSpec/NoExpectationExample: + Enabled: false + +RSpec/MessageSpies: + Enabled: false + +RSpec/InstanceVariable: + Enabled: false + +RSpec/ContextWording: + Enabled: false + +RSpec/SpecFilePathFormat: + Enabled: false + +RSpec/MultipleMemoizedHelpers: + Enabled: false + +RSpec/BeforeAfterAll: + Enabled: false + +RSpec/DescribeMethod: + Enabled: false diff --git a/hue/Gemfile b/hue/Gemfile @@ -3,3 +3,24 @@ source 'https://rubygems.org' gemspec + +group :development do + gem 'debug', '>= 1.0' + gem 'rake' + gem 'rspec', '>= 3.0' + gem 'ruby-lsp' + gem 'webmock', '>= 3.0' + + gem 'reek' + gem 'rubocop' + gem 'rubocop-rake' + gem 'rubocop-rspec' + + gem 'rbs' + gem 'sorbet' + gem 'sorbet-runtime' + gem 'tapioca' + + gem 'bundler-audit' + gem 'simplecov' +end diff --git a/hue/Gemfile.lock b/hue/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - hue (0.4.0) + hue (1.0.0) json (~> 2.18) matrix @@ -10,16 +10,102 @@ GEM specs: addressable (2.9.0) public_suffix (>= 2.0.2, < 8.0) + ast (2.4.3) + benchmark (0.5.0) bigdecimal (4.1.2) + bundler-audit (0.9.3) + bundler (>= 1.2.0) + thor (~> 1.0) + concurrent-ruby (1.3.6) crack (1.0.1) bigdecimal rexml + date (3.5.1) + debug (1.11.1) + irb (~> 1.10) + reline (>= 0.3.8) diff-lcs (1.6.2) + docile (1.4.1) + dry-configurable (1.3.0) + dry-core (~> 1.1) + zeitwerk (~> 2.6) + dry-core (1.2.0) + concurrent-ruby (~> 1.0) + logger + zeitwerk (~> 2.6) + dry-inflector (1.3.1) + dry-initializer (3.2.0) + dry-logic (1.6.0) + bigdecimal + concurrent-ruby (~> 1.0) + dry-core (~> 1.1) + zeitwerk (~> 2.6) + dry-schema (1.16.0) + concurrent-ruby (~> 1.0) + dry-configurable (~> 1.0, >= 1.0.1) + dry-core (~> 1.1) + dry-initializer (~> 3.2) + dry-logic (~> 1.6) + dry-types (~> 1.9, >= 1.9.1) + zeitwerk (~> 2.6) + dry-types (1.9.1) + bigdecimal (>= 3.0) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0) + dry-inflector (~> 1.0) + dry-logic (~> 1.4) + zeitwerk (~> 2.6) + erb (6.0.4) + erubi (1.13.1) hashdiff (1.2.1) + io-console (0.8.2) + irb (1.18.0) + pp (>= 0.6.0) + prism (>= 1.3.0) + rdoc (>= 4.0.0) + reline (>= 0.4.2) json (2.19.4) + language_server-protocol (3.17.0.5) + lint_roller (1.1.0) + logger (1.7.0) matrix (0.4.3) + netrc (0.11.0) + parallel (2.1.0) + parser (3.3.11.1) + ast (~> 2.4.1) + racc + pp (0.6.3) + prettyprint + prettyprint (0.2.0) + prism (1.9.0) + psych (5.3.1) + date + stringio public_suffix (7.0.5) + racc (1.8.1) + rainbow (3.1.1) rake (13.4.2) + rbi (0.3.11) + prism (~> 1.0) + rbs (>= 4.0.1) + rbs (4.0.2) + logger + prism (>= 1.6.0) + tsort + rdoc (7.2.0) + erb + psych (>= 4.0.0) + tsort + reek (6.5.0) + dry-schema (~> 1.13) + logger (~> 1.6) + parser (~> 3.3.0) + rainbow (>= 2.0, < 4.0) + rexml (~> 3.1) + regexp_parser (2.12.0) + reline (0.6.3) + io-console (~> 0.5) + require-hooks (0.4.0) rexml (3.4.4) rspec (3.13.2) rspec-core (~> 3.13.0) @@ -34,39 +120,176 @@ GEM diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) rspec-support (3.13.7) + rubocop (1.86.1) + json (~> 2.3) + language_server-protocol (~> 3.17.0.2) + lint_roller (~> 1.1.0) + parallel (>= 1.10) + parser (>= 3.3.0.2) + rainbow (>= 2.2.2, < 4.0) + regexp_parser (>= 2.9.3, < 3.0) + rubocop-ast (>= 1.49.0, < 2.0) + ruby-progressbar (~> 1.7) + unicode-display_width (>= 2.4.0, < 4.0) + rubocop-ast (1.49.1) + parser (>= 3.3.7.2) + prism (~> 1.7) + rubocop-rake (0.7.1) + lint_roller (~> 1.1) + rubocop (>= 1.72.1) + rubocop-rspec (3.9.0) + lint_roller (~> 1.1) + rubocop (~> 1.81) + ruby-lsp (0.26.9) + language_server-protocol (~> 3.17.0) + prism (>= 1.2, < 2.0) + rbs (>= 3, < 5) + ruby-progressbar (1.13.0) + rubydex (0.2.2-x86_64-linux) + simplecov (0.22.0) + docile (~> 1.1) + simplecov-html (~> 0.11) + simplecov_json_formatter (~> 0.1) + simplecov-html (0.13.2) + simplecov_json_formatter (0.1.4) + sorbet (0.6.13208) + sorbet-static (= 0.6.13208) + sorbet-runtime (0.6.13208) + sorbet-static (0.6.13208-x86_64-linux) + sorbet-static-and-runtime (0.6.13208) + sorbet (= 0.6.13208) + sorbet-runtime (= 0.6.13208) + spoom (1.7.14) + erubi (>= 1.10.0) + prism (>= 0.28.0) + rbi (>= 0.3.3) + rbs (>= 4.0.0.dev.5) + rexml (>= 3.2.6) + sorbet-static-and-runtime (>= 0.5.10187) + thor (>= 0.19.2) + stringio (3.2.0) + tapioca (0.19.1) + benchmark + bundler (>= 2.2.25) + netrc (>= 0.11.0) + parallel (>= 1.21.0) + rbi (>= 0.3.7) + require-hooks (>= 0.2.2) + rubydex (>= 0.1.0.beta10) + sorbet-static-and-runtime (>= 0.6.12698) + spoom (>= 1.7.9) + thor (>= 1.2.0) + tsort + thor (1.5.0) + tsort (0.2.0) + unicode-display_width (3.2.0) + unicode-emoji (~> 4.1) + unicode-emoji (4.2.0) webmock (3.26.2) addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) + zeitwerk (2.7.5) PLATFORMS - ruby x86_64-linux DEPENDENCIES + bundler-audit + debug (>= 1.0) hue! rake + rbs + reek rspec (>= 3.0) + rubocop + rubocop-rake + rubocop-rspec + ruby-lsp + simplecov + sorbet + sorbet-runtime + tapioca webmock (>= 3.0) CHECKSUMS addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af + ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383 + benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd + bundler (4.0.11) sha256=5bcec0fb78302e48d02ee46f10ee6e6942be647ba5b44a6d1ddfda9a240ce785 + bundler-audit (0.9.3) sha256=81c8766c71e47d0d28a0f98c7eed028539f21a6ea3cd8f685eb6f42333c9b4e9 + concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab crack (1.0.1) sha256=ff4a10390cd31d66440b7524eb1841874db86201d5b70032028553130b6d4c7e + date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0 + debug (1.11.1) sha256=2e0b0ac6119f2207a6f8ac7d4a73ca8eb4e440f64da0a3136c30343146e952b6 diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962 + docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e + dry-configurable (1.3.0) sha256=882d862858567fc1210d2549d4c090f34370fc1bb7c5c1933de3fe792e18afa8 + dry-core (1.2.0) sha256=0cc5a7da88df397f153947eeeae42e876e999c1e30900f3c536fb173854e96a1 + dry-inflector (1.3.1) sha256=7fb0c2bb04f67638f25c52e7ba39ab435d922a3a5c3cd196120f63accb682dcc + dry-initializer (3.2.0) sha256=37d59798f912dc0a1efe14a4db4a9306989007b302dcd5f25d0a2a20c166c4e3 + dry-logic (1.6.0) sha256=da6fedbc0f90fc41f9b0cc7e6f05f5d529d1efaef6c8dcc8e0733f685745cea2 + dry-schema (1.16.0) sha256=cd3aaeabc0f1af66ec82a29096d4c4fb92a0a58b9dae29a22b1bbceb78985727 + dry-types (1.9.1) sha256=baebeecdb9f8395d6c9d227b62011279440943e3ef2468fe8ccc1ba11467f178 + erb (6.0.4) sha256=38e3803694be357fe2bfe312487c74beaf9fb4e5beb3e22498952fe1645b95d9 + erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9 hashdiff (1.2.1) sha256=9c079dbc513dfc8833ab59c0c2d8f230fa28499cc5efb4b8dd276cf931457cd1 - hue (0.4.0) + hue (1.0.0) + io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc + irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3 json (2.19.4) sha256=670a7d333fb3b18ca5b29cb255eb7bef099e40d88c02c80bd42a3f30fe5239ac + language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc + lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87 + logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 matrix (0.4.3) sha256=a0d5ab7ddcc1973ff690ab361b67f359acbb16958d1dc072b8b956a286564c5b + netrc (0.11.0) sha256=de1ce33da8c99ab1d97871726cba75151113f117146becbe45aa85cb3dabee3f + parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356 + parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54 + pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6 + prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193 + prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85 + psych (5.3.1) sha256=eb7a57cef10c9d70173ff74e739d843ac3b2c019a003de48447b2963d81b1974 public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623 + racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f + rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701 + rbi (0.3.11) sha256=d8f0f1b45f8b470dc3bdd1e8192b1d554c826d66a4fb73e68480c4230c638d02 + rbs (4.0.2) sha256=af75671e66cd03434cc546622741ebf83f6197ec4328375805306330bf78ef25 + rdoc (7.2.0) sha256=8650f76cd4009c3b54955eb5d7e3a075c60a57276766ebf36f9085e8c9f23192 + reek (6.5.0) sha256=d26d3a492773b2bbc228888067a21afe33ac07954a17dbd64cdeae42c4c69be1 + regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb + reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835 + require-hooks (0.4.0) sha256=005f4c6435b4edae73e358cdbaba48370a4121f9ce893d5d2a3c66fce855677d rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142 rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587 rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836 rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47 rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c + rubocop (1.86.1) sha256=44415f3f01d01a21e01132248d2fd0867572475b566ca188a0a42133a08d4531 + rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035 + rubocop-rake (0.7.1) sha256=3797f2b6810c3e9df7376c26d5f44f3475eda59eb1adc38e6f62ecf027cbae4d + rubocop-rspec (3.9.0) sha256=8fa70a3619408237d789aeecfb9beef40576acc855173e60939d63332fdb55e2 + ruby-lsp (0.26.9) sha256=33a01c001c00a76b4e821efc04ed7572983430f31ca5d6f3e343d0b6ccab4129 + ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33 + rubydex (0.2.2-x86_64-linux) sha256=b356fbd11401522dc6967637b3c0485827053e549303229eeb8aa669638b0885 + simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5 + simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246 + simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428 + sorbet (0.6.13208) sha256=040b8de0b8746a2c7a0cabaab6f38b8508c215f344372bf818245986a9ee7240 + sorbet-runtime (0.6.13208) sha256=2ae9a6e52bbfdd39916448b09bfb165357190effb28fd8c554c5dd2c52be0a32 + sorbet-static (0.6.13208-x86_64-linux) sha256=bc210d5256c877106c21643f8d5fa8c5b445d5e1a825c0b3b1d0030aed128b09 + sorbet-static-and-runtime (0.6.13208) sha256=31aa780d04cc6abf39f062cd334bcd243ac8566107ca37440883cac82bae88b7 + spoom (1.7.14) sha256=48da4e39d9a63611aa0132a3656d7e75833fcccb6006d59f7fa879b98c8877c4 + stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1 + tapioca (0.19.1) sha256=5de94d458950897ffe3d4e86a21eec48bb6a8775af85f80b1486b7ae7ba51823 + thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73 + tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f + unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42 + unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f webmock (3.26.2) sha256=774556f2ea6371846cca68c01769b2eac0d134492d21f6d0ab5dd643965a4c90 + zeitwerk (2.7.5) sha256=d8da92128c09ea6ec62c949011b00ed4a20242b255293dd66bf41545398f73dd BUNDLED WITH - 4.0.7 + 4.0.11 diff --git a/hue/Rakefile b/hue/Rakefile @@ -42,4 +42,23 @@ task :clean do rm_rf 'pkg' end -task default: [:spec] +require 'rubocop/rake_task' +RuboCop::RakeTask.new + +require 'reek/rake/task' +Reek::Rake::Task.new + +desc 'Run bundler-audit' +task :audit do + sh 'bundle exec bundle-audit check --update' +end + +desc 'Run sorbet typecheck' +task :sorbet do + sh 'bundle exec srb tc' +end + +desc 'Run all static analysis' +task lint: %i[rubocop reek sorbet audit] + +task default: %i[spec rubocop] diff --git a/hue/hue-0.4.0.gem b/hue/hue-0.4.0.gem Binary files differ. diff --git a/hue/hue.gemspec b/hue/hue.gemspec @@ -4,10 +4,11 @@ $LOAD_PATH.unshift File.expand_path('lib', __dir__) Gem::Specification.new do |s| s.name = 'hue' - s.version = '0.4.0' + s.version = '1.0.0' s.authors = ['mtmn'] - s.email = ['miro@mtmn.name'] - s.homepage = 'https://codeberg.org/mtmn/tools' + s.email = ['miro@haravara.org'] + s.homepage = 'https://git.sr.ht/~mtmn/tools' + s.licenses = ['MIT'] s.summary = 'Ruby library and CLI for controlling Philips Hue lights and bridge.' s.description = 'Library and command line interface for controlling the Philips Hue system. ' \ 'Allows registration and invocation of a registered Philips Hue app, with ' \ @@ -21,9 +22,5 @@ Gem::Specification.new do |s| s.add_dependency('json', '~> 2.18') s.add_dependency('matrix') - s.add_development_dependency('rake') - s.add_development_dependency('rspec', '>= 3.0') - s.add_development_dependency('webmock', '>= 3.0') - s.required_ruby_version = '>= 3.0' end diff --git a/hue/lib/hue.rb b/hue/lib/hue.rb @@ -39,10 +39,10 @@ module Hue def self.application application_config = Hue::Config::Application.default - bridge_config = Hue::Config::Bridge.find(application_config.bridge_id) - bridge_config ||= register_bridges[application_config.bridge_id] + bridge_id = application_config.bridge_id + bridge_config = Hue::Config::Bridge.find(bridge_id) || register_bridges[bridge_id] - raise Error, "Unable to find bridge: #{application_config.bridge_id}" if bridge_config.nil? + raise Error, "Unable to find bridge: #{bridge_id}" unless bridge_config Hue::Bridge.new(application_config.id, bridge_config.uri) end @@ -61,39 +61,41 @@ module Hue bridges end - def self.udp_discover(bridges) - Hue.logger.info('Bridge UDP Discovery') - payload = <<~PAYLOAD - M-SEARCH * HTTP/1.1 - ST: ssdp:all - MX: 10 - MAN: ssdp:discover - HOST: 239.255.255.250:1900 - PAYLOAD - broadcast_address = '239.255.255.250' - port_number = 1900 + UDP_SSDP_PAYLOAD = <<~PAYLOAD + M-SEARCH * HTTP/1.1 + ST: ssdp:all + MX: 10 + MAN: ssdp:discover + HOST: 239.255.255.250:1900 + PAYLOAD + UDP_SSDP_ADDRESS = '239.255.255.250' + UDP_SSDP_PORT = 1900 + def self.udp_discover(bridges) + log = Hue.logger + log.info('Bridge UDP Discovery') socket = UDPSocket.new(Socket::AF_INET) - socket.send(payload, 0, broadcast_address, port_number) - - Timeout.timeout(DEFAULT_UDP_TIMEOUT, Hue::Error) do - loop do - message, (_, port, hostname, ip_add) = socket.recvfrom(1024) - if message =~ /IpBridge/ && /LOCATION: (.*)$/.match(message) - if (uuid_match = /uuid:(.{36})/.match(message)) - # Assume this is Philips Hue for now. - uuid = uuid_match.captures.first - logger.info("Found bridge (#{hostname}:#{port}) with uuid: #{uuid}") if bridges[uuid].nil? - bridges[uuid] = "http://#{ip_add}/api" - end - else - logger.debug("Found #{hostname}:#{port}: #{message}") - end + socket.send(UDP_SSDP_PAYLOAD, 0, UDP_SSDP_ADDRESS, UDP_SSDP_PORT) + Timeout.timeout(DEFAULT_UDP_TIMEOUT, Hue::Error) { listen_for_bridges(socket, bridges, log) } + rescue Hue::Error => e + log.warn(e) + log.info('UDPSocket timed out.') + end + + def self.listen_for_bridges(socket, bridges, log) + loop do + message, (_, port, hostname, ip_add) = socket.recvfrom(1024) + if message =~ /IpBridge/ && /LOCATION: (.*)$/.match(message) + uuid_match = /uuid:(.{36})/.match(message) + next unless uuid_match + + uuid = uuid_match.captures.first + log.info("Found bridge (#{hostname}:#{port}) with uuid: #{uuid}") unless bridges[uuid] + bridges[uuid] = "http://#{ip_add}/api" + else + log.debug("Found #{hostname}:#{port}: #{message}") end end - rescue Hue::Error => e - Hue.logger.warn(e) - logger.info('UDPSocket timed out.') end def self.nupnp_discover(bridges) @@ -106,12 +108,12 @@ module Hue rescue StandardError nil end - return if json.nil? + return unless json json.each do |bridge| uuid = bridge['id'] ip_add = bridge['internalipaddress'] - bridges[uuid] = "http://#{ip_add}/api" unless uuid.nil? || ip_add.nil? + bridges[uuid] = "http://#{ip_add}/api" if uuid && ip_add end end @@ -135,11 +137,7 @@ module Hue end def to_s - if @original_error.nil? - super - else - "#{super}\nCause: #{@original_error}" - end + @original_error ? "#{super}\nCause: #{@original_error}" : super end end @@ -155,14 +153,7 @@ module Hue def self.logger @logger ||= begin - log_dir_path = File.join(ENV['HOME'], '.hue') - begin - FileUtils.mkdir_p(log_dir_path) - rescue Errno::EACCES - log_dir_path = File.join(ENV['HOME'], ".#{device_type}") - FileUtils.mkdir_p(log_dir_path) - end - + log_dir_path = ensure_log_dir log_file_path = File.join(log_dir_path, 'hue.log') log_file = File.new(log_file_path, File::WRONLY | File::APPEND | File::CREAT) logger = Logger.new(log_file) @@ -171,6 +162,17 @@ module Hue end end + def self.ensure_log_dir + home = Dir.home + [File.join(home, '.hue'), File.join(home, ".#{device_type}")].each do |path| + FileUtils.mkdir_p(path) + return path + rescue Errno::EACCES + next + end + raise Error, 'Unable to create log directory' + end + def self.percent_to_unit_interval(value) if (percent = /(\d+)%/.match(value.to_s)) percent.captures.first.to_i / 100.0 diff --git a/hue/lib/hue/animations/sunrise.rb b/hue/lib/hue/animations/sunrise.rb @@ -13,31 +13,34 @@ module Hue def perform_sunrise(total_time_in_minutes = 18) multiplier = sunrise_multiplier total_time_in_minutes steps = sunrise_steps(multiplier) - if on? - puts "ON! #{steps[0][:bri]} :: #{brightness} :: #{brightness > steps[0][:bri]}" - steps.shift while brightness >= steps[0][:bri] - end - steps.each_with_index do |step, _i| - update step.merge(on: true) - sleep(step[:transitiontime] / 10.0) - end + skip_until_brightness(steps, :>=) + run_steps(steps) end def perform_sunset(total_time_in_minutes = 18) multiplier = sunrise_multiplier total_time_in_minutes steps = sunset_steps(multiplier) - if on? - puts "ON! #{steps[0][:bri]} :: #{brightness} :: #{brightness > steps[0][:bri]}" - steps.shift while brightness <= steps[0][:bri] - end - steps.each_with_index do |step, _i| + skip_until_brightness(steps, :<=) + run_steps(steps) + off + end + + private + + def run_steps(steps) + steps.each do |step| update step.merge(on: true) sleep(step[:transitiontime] / 10.0) end - off end - private + # :reek:DuplicateMethodCall + def skip_until_brightness(steps, comparison) + return unless on? + + puts "ON! #{steps[0][:bri]} :: #{brightness} :: #{brightness > steps[0][:bri]}" + steps.shift while brightness.public_send(comparison, steps[0][:bri]) + end def sunrise_multiplier(total_time_in_minutes) # total_time / 18 steps == time_per_step diff --git a/hue/lib/hue/bridge.rb b/hue/lib/hue/bridge.rb @@ -60,9 +60,10 @@ module Hue def register response = create(URI.parse(bridge_uri), { 'devicetype' => "#{Hue.device_type}@#{Socket.gethostname}" }) - return unless response.first.key?('success') + first = response.first + return unless first.key?('success') - @application_id = response.first['success']['username'] + @application_id = first['success']['username'] end def unregister @@ -104,32 +105,36 @@ module Hue end def receive(request_class, url, payload = nil) - request = request_class.new(url.request_uri, { 'Content-Type' => 'application/json' }) - request.body = payload if payload - Hue.logger.info("Sending #{payload} to #{url}") - response = nil - begin - http = Net::HTTP.new(url.host, url.port) - http.open_timeout = 3 # Quick timeout on connection fail. - http.read_timeout = 8 # Slower timeout on read fail, but way faster than the default. - response = http.start { |http| http.request(request) } - rescue StandardError => e - Hue.logger.error(e.message) - raise Hue::Error.new('Problem reaching bridge.', e) - end + log = Hue.logger + log.info("Sending #{payload} to #{url}") - if response && response.code.to_s != '200' - Hue.logger.info("Error with response #{response.code} #{response.message}") - raise Hue::Error, "Unexpected response: #{response.code}, #{response.message}" - else - json = JSON.parse(response.body) - Hue.logger.info("Response #{response.code} #{response.message}: #{json}") - if json.is_a?(Array) && (error = json.first['error']) - raise Hue::API::Error, error - else - json - end + response = send_request(request_class, url, payload) + code = response.code.to_s + message = response.message + + if code != '200' + log.info("Error with response #{code} #{message}") + raise Hue::Error, "Unexpected response: #{code}, #{message}" end + + json = JSON.parse(response.body) + log.info("Response #{code} #{message}: #{json}") + first_error = json.first['error'] if json.is_a?(Array) + raise Hue::API::Error, first_error if first_error + + json + end + + def send_request(request_class, url, payload) + request = request_class.new(url.request_uri, { 'Content-Type' => 'application/json' }) + request.body = payload if payload + http = Net::HTTP.new(url.host, url.port) + http.open_timeout = 3 + http.read_timeout = 8 + http.start { |conn| conn.request(request) } + rescue StandardError => e + Hue.logger.error(e.message) + raise Hue::Error.new('Problem reaching bridge.', e) end end end diff --git a/hue/lib/hue/cli.rb b/hue/lib/hue/cli.rb @@ -48,7 +48,8 @@ module Hue end def handle_bridge_error(error, original_args) - return puts error.message unless ERROR_BRIDGE_CONNECTION_PROBLEM.match?(error.message) + message = error.message + return puts message unless ERROR_BRIDGE_CONNECTION_PROBLEM.match?(message) @retry_count += 1 diff --git a/hue/lib/hue/cli/commands/delete.rb b/hue/lib/hue/cli/commands/delete.rb @@ -6,12 +6,8 @@ module Hue class Delete < Hue::CLI::Command def execute(*args) app_id = args.shift - - if app_id.nil? - bridge.unregister - else - Bridge.new(app_id, bridge.bridge_uri).unregister - end + target = app_id ? Bridge.new(app_id, bridge.bridge_uri) : bridge + target.unregister end protected diff --git a/hue/lib/hue/cli/commands/light.rb b/hue/lib/hue/cli/commands/light.rb @@ -70,15 +70,9 @@ module Hue end def clear(*args) - case args.first - when 'effect' - bulb.clear_effect - when 'action' - bulb.solid - else - bulb.clear_effect - bulb.solid - end + target = args.first + bulb.clear_effect if target != 'action' + bulb.solid if target != 'effect' end def dump(*_args) diff --git a/hue/lib/hue/cli/processors/light_state_alias.rb b/hue/lib/hue/cli/processors/light_state_alias.rb @@ -9,8 +9,8 @@ module Hue ALIAS_FILE_NAME = 'aliases.yml' ALIAS_FILE_PATHS = [ File.join(Hue::CLI::LOCATION, 'lib', 'config'), # Default (bundled with gem) - '/etc/hue/', # System - File.join(ENV['HOME'], '.hue') # User + '/etc/hue/', # System + File.join(Dir.home, '.hue') # User ].freeze @alias_map = {} @@ -43,8 +43,9 @@ module Hue def initialize(name) @name = name - @state = self.class.alias_map[name.to_sym] - raise Error, "Unknown light state: #{name}" if @state.nil? + @state = self.class.alias_map.fetch(name.to_sym) do + raise Error, "Unknown light state: #{name}" + end end end end diff --git a/hue/lib/hue/colors.rb b/hue/lib/hue/colors.rb @@ -13,13 +13,8 @@ module Hue when 1 Colors::ColorTemperature.new(args.first) when 2 - a = args.first.to_f - _ = args.last.to_f - if a > 1.0 - Colors::HueSaturation.new(args.first, args.last) - else - Colors::XY.new(*args) - end + first, last = args + first.to_f > 1.0 ? Colors::HueSaturation.new(first, last) : Colors::XY.new(first, last) when 3 Colors::RGB.new(*args) else diff --git a/hue/lib/hue/colors/color.rb b/hue/lib/hue/colors/color.rb @@ -6,7 +6,7 @@ module Hue ERROR_METHOD_NOT_IMPLEMENTED = 'method-not-implemented' def self.ranged(min, val, max) - [[min, val].max, max].min + val.clamp(min, max) end def to_hash diff --git a/hue/lib/hue/colors/color_temperature.rb b/hue/lib/hue/colors/color_temperature.rb @@ -52,21 +52,17 @@ module Hue # using method described at # http://www.tannerhelland.com/4435/convert-temperature-rgb-algorithm-code/ temp = kelvin / 100 + cool = temp <= 66 + offset = temp - 60 - red = temp <= 66 ? 255 : 329.698727446 * ((temp - 60)**-0.1332047592) - - green = if temp <= 66 - 99.4708025861 * Math.log(temp) - 161.1195681661 - else - 288.1221695283 * ((temp - 60)**-0.0755148492) - end - + red = cool ? 255 : 329.698727446 * (offset**-0.1332047592) + green = cool ? (99.4708025861 * Math.log(temp)) - 161.1195681661 : 288.1221695283 * (offset**-0.0755148492) blue = if temp >= 66 255 elsif temp <= 19 0 else - 138.5177312231 * Math.log(temp - 10) - 305.0447927307 + (138.5177312231 * Math.log(temp - 10)) - 305.0447927307 end RGB.new(red, green, blue) @@ -75,7 +71,7 @@ module Hue private def unit_to_mired_interval(unit_interval) - unit_interval * (MIRED_MAX - MIRED_MIN) + MIRED_MIN + (unit_interval * (MIRED_MAX - MIRED_MIN)) + MIRED_MIN end end end diff --git a/hue/lib/hue/colors/hue_saturation.rb b/hue/lib/hue/colors/hue_saturation.rb @@ -66,11 +66,12 @@ module Hue else v = 1.0 # We are setting the value to 1. Don't count brightness here - i = (h * 6).floor - f = h * 6 - i + h6 = h * 6 + i = h6.floor + f = h6 - i p = v * (1 - s) - q = v * (1 - f * s) - t = v * (1 - (1 - f) * s) + q = v * (1 - (f * s)) + t = v * (1 - ((1 - f) * s)) case i % 6 when 0 @@ -106,11 +107,11 @@ module Hue private def unit_to_hue_interval(value) - (value * (HUE_MAX - HUE_MIN) + HUE_MIN).round + ((value * (HUE_MAX - HUE_MIN)) + HUE_MIN).round end def unit_to_saturation_interval(value) - (value * (SATURATION_MAX - SATURATION_MIN) + SATURATION_MIN).round + ((value * (SATURATION_MAX - SATURATION_MIN)) + SATURATION_MIN).round end end end diff --git a/hue/lib/hue/colors/rgb.rb b/hue/lib/hue/colors/rgb.rb @@ -33,16 +33,13 @@ module Hue end def to_hash - max = MAX.to_f - red = self.red / max - green = self.green / max - blue = self.red / max + scale = MAX.to_f + red = self.red / scale + green = self.green / scale + blue = self.blue / scale max = [red, green, blue].max min = [red, green, blue].min - _ = 0 - _ = ((max + min) / 2 * 255) - d = max - min s = max.zero? ? 0 : (d / max * 255) @@ -50,11 +47,11 @@ module Hue when min 0 # monochromatic when red - (green - blue) / d + (green < blue ? 6 : 0) + ((green - blue) / d) + (green < blue ? 6 : 0) when green - (blue - red) / d + 2 + ((blue - red) / d) + 2 when blue - (red - green) / d + 4 + ((red - green) / d) + 4 end * 60 h = (h * HueSaturation::HUE_SCALE).to_i @@ -87,7 +84,7 @@ module Hue end def unit_to_rgb_scale(value) - (value * (MAX - MIN) + MIN).round + ((value * (MAX - MIN)) + MIN).round end def parse(value) diff --git a/hue/lib/hue/colors/xy.rb b/hue/lib/hue/colors/xy.rb @@ -14,10 +14,10 @@ module Hue attr_reader :x, :y - def initialize(*xy) + def initialize(*coords) super() - self.x = xy.first - self.y = xy.last + self.x = coords.first + self.y = coords.last end def x=(value) diff --git a/hue/lib/hue/config/abstract.rb b/hue/lib/hue/config/abstract.rb @@ -25,7 +25,8 @@ module Hue yaml ||= {} if yaml.key?(name) && !overwrite_existing_key - raise "Key named '#{name}' already exists in config file '#{path}'.\nPlease remove it before creating a new one with the same name." + raise "Key named '#{name}' already exists in config file '#{path}'.\n" \ + 'Please remove it before creating a new one with the same name.' end add_self_to_yaml(yaml) @@ -36,7 +37,7 @@ module Hue yaml = begin YAML.load_file(path) rescue StandardError - Hash::New + {} end yaml.delete(name) if yaml.key?(name) @@ -65,7 +66,7 @@ module Hue class << self def setup_file_path(path) dir = File.dirname(path) - FileUtils.mkdir_p(dir) unless Dir.exist?(dir) + FileUtils.mkdir_p(dir) end def read_file(config_file) diff --git a/hue/lib/hue/config/application.rb b/hue/lib/hue/config/application.rb @@ -8,7 +8,7 @@ module Hue STRING_ID = 'id' def self.hue_config_dir - File.join(ENV['HOME'], '.hue') + File.join(Dir.home, '.hue') end def self.file_path @@ -42,7 +42,7 @@ module Hue end def ==(other) - super(other) && + super && bridge_id == other.bridge_id && id == other.id end @@ -53,7 +53,7 @@ module Hue key = name.dup.force_encoding('ASCII') # Avoid binary encoded YAML bridge = bridge_id.dup.force_encoding('ASCII') yaml[key] = { - STRING_ID => id.force_encoding('ASCII'), + STRING_ID => id.dup.force_encoding('ASCII'), STRING_BRIDGE_ID => bridge } end diff --git a/hue/lib/hue/config/bridge.rb b/hue/lib/hue/config/bridge.rb @@ -4,7 +4,7 @@ module Hue module Config class Bridge < Abstract def self.hue_config_dir - File.join(ENV['HOME'], '.hue') + File.join(Dir.home, '.hue') end def self.file_path @@ -33,7 +33,7 @@ module Hue end def write(overwrite_existing_key: true) - super(overwrite_existing_key: overwrite_existing_key) + super end private diff --git a/hue/lib/hue/utilities/indentation.rb b/hue/lib/hue/utilities/indentation.rb @@ -15,14 +15,16 @@ module Indentation reset + # :reek:UtilityFunction { exclude: [ "Indentation#indent" ] } def indent(size = DEFAULT_INDENTATION_SIZE) original_size = Indentation.indentation_size + original_level = Indentation.indentation_level Indentation.indentation_size = size - Indentation.indentation_level += 1 + Indentation.indentation_level = original_level + 1 yield ensure Indentation.indentation_size = original_size - Indentation.indentation_level -= 1 + Indentation.indentation_level = original_level end def puts(*message) diff --git a/hue/pkg/hue-0.1.0.gem b/hue/pkg/hue-0.1.0.gem Binary files differ. diff --git a/hue/sig/.keep b/hue/sig/.keep diff --git a/hue/sorbet/config b/hue/sorbet/config @@ -0,0 +1,7 @@ +--dir +. +--ignore=/pkg/ +--ignore=/vendor/ +--ignore=/coverage/ +--ignore=/tmp/ +--ignore=/spec/ diff --git a/hue/spec/cli_spec.rb b/hue/spec/cli_spec.rb @@ -4,12 +4,12 @@ require 'spec_helper' describe Hue::CLI do it 'can be loaded' do - expect(Hue::CLI).to be_a(Module) + expect(described_class).to be_a(Module) end it 'has valid commands' do # Trigger object space loading - Hue::CLI.send(:commands_in_object_space) + described_class.send(:commands_in_object_space) # Just ensure it doesn't crash end end diff --git a/hue/spec/hue/bridge_spec.rb b/hue/spec/hue/bridge_spec.rb @@ -1,57 +1,56 @@ -require 'spec_helper.rb' +# frozen_string_literal: true -describe Hue::Bridge do +require 'spec_helper' +describe Hue::Bridge do context 'when instantiated' do let(:bridge) { test_bridge } - it 'should report the status' do + it 'reports the status' do with_fake_request expect(bridge.status).to eq(api_reply(:get_success)) end - it 'should report errors' do + it 'reports errors' do with_fake_request(:lights, :unauthorized) - expect { + expect do bridge.lights - }.to raise_error(Hue::API::Error, 'unauthorized user') + end.to raise_error(Hue::API::Error, 'unauthorized user') end - it 'should report the available lights' do + it 'reports the available lights' do with_fake_request(:lights) expect(bridge.lights).to eq(api_reply(:lights)) end - it 'should report a simple string of light names' do + it 'reports a simple string of light names' do with_fake_request(:lights) expect(bridge.light_names).to eq("1. Dining\n2. Bedroom Far\n3. Bedroom Near") end - it 'should report the bridge config' do + it 'reports the bridge config' do with_fake_request(:config) expect(bridge.config).to eq(api_reply(:config)) end - it 'should report the light schedules' do + it 'reports the light schedules' do with_fake_request(:schedules) expect(bridge.schedules).to eq(api_reply(:schedules)) end - it 'should return instance of all the bulbs' do + it 'returns instance of all the bulbs' do with_fake_request(:lights) bulbs = bridge.bulbs expect(bulbs.size).to eq(3) - bulbs.each do |bulb| - expect(bulb).to be_a(Hue::Bulb) - end + expect(bulbs).to all(be_a(Hue::Bulb)) end - it 'should allow unregistering an existing config' do + it 'allows unregistering an existing config' do with_fake_delete("config/whitelist/#{TEST_APPLICATION_UUID}") bridge.unregister end - it 'should allow scanning for new lights' do + it 'allows scanning for new lights' do with_fake_post("#{TEST_APPLICATION_UUID}/lights") bridge.add_lights end @@ -61,11 +60,10 @@ describe Hue::Bridge do let(:new_id) { 'new_test_id' } let(:bridge) { described_class.new(new_id, TEST_BRIDGE_URI) } - it 'should allow registering the new config' do + it 'allows registering the new config' do mock_socket_hostname - with_fake_post(nil, {:devicetype => "hue-lib@hostname"}) + with_fake_post(nil, { devicetype: "#{Hue::DEVICE_TYPE}@hostname" }) bridge.register end end - end diff --git a/hue/spec/hue/bulb_spec.rb b/hue/spec/hue/bulb_spec.rb @@ -1,72 +1,72 @@ -require 'spec_helper.rb' +# frozen_string_literal: true -describe Hue::Bulb do +require 'spec_helper' +describe Hue::Bulb do context 'when instantiated with a given bridge and id' do let(:bulb) { described_class.new(test_bridge, 1) } - before(:each) do + before do with_fake_request('lights/1') @test_status = api_reply('lights/1') bulb.refresh! end - it 'should report the bulb state' do + it 'reports the bulb state' do expect(bulb.state).to eq(@test_status['state']) end - it 'should report the bulb info' do + it 'reports the bulb info' do info = api_reply('lights/1') info.delete('state') info.delete('pointsymbol') expect(bulb.info).to eq(info) end - it "should report it's name" do + it "reports it's name" do expect(bulb.name).to eq(@test_status['name']) end - it "should report if it's on" do + it "reports if it's on" do expect(bulb.on?).to be false end - it "should report if it's off" do + it "reports if it's off" do expect(bulb.off?).to be true end - it "should report the brightness and color mode" do + it 'reports the brightness and color mode' do expect(bulb.brightness).to eq(146) expect(bulb.bri).to eq(146) expect(bulb.color_mode).to eq('ct') expect(bulb.color_mode).to eq(bulb.colormode) end - it "should report the color" do + it 'reports the color' do color = bulb.color expect(color).to be_a(Hue::Colors::ColorTemperature) expect(color.mired).to eq(459) expect(color.kelvin).to eq(2179) end - it "should report the alert state" do + it 'reports the alert state' do expect(bulb.alert).to eq('none') expect(bulb.blinking?).to be false expect(bulb.solid?).to be true end - it "should report the effect state" do + it 'reports the effect state' do expect(bulb.effect?).to be false expect(bulb.effect).to eq('none') expect(bulb.color_loop?).to be false end - it "should report the transition time" do + it 'reports the transition time' do expect(bulb.transition_time).to eq(0) end context 'by changing state' do - - it 'should allow turning bulps on and off' do + it 'allows turning bulps on and off' do with_fake_update('lights/1/state', on: true) expect(bulb.on).to be true @@ -74,38 +74,38 @@ describe Hue::Bulb do expect(bulb.off).to be true end - it 'should allow changing the name' do + it 'allows changing the name' do with_fake_update('lights/1', name: 'New name') bulb.name = 'New name' expect(bulb.name).to eq('New name') end - it 'should allow setting hue, saturation and brightness' do - color = Hue::Colors::HueSaturation.new(21845, 1293) + it 'allows setting hue, saturation and brightness' do + color = Hue::Colors::HueSaturation.new(21_845, 1293) - with_fake_update('lights/1/state', hue: 21845, sat: 255) + with_fake_update('lights/1/state', hue: 21_845, sat: 255) set_color = (bulb.color = color) - expect(set_color.hue).to eq(21845) + expect(set_color.hue).to eq(21_845) expect(set_color.saturation).to eq(255) end - it 'should allow setting brightness as a number, percentage or string' do + it 'allows setting brightness as a number, percentage or string' do with_fake_update('lights/1/state', bri: 233) bulb.brightness = 233 expect(bulb.brightness).to eq(233) with_fake_update('lights/1/state', bri: 128) - bulb.brightness = "50%" + bulb.brightness = '50%' expect(bulb.brightness).to eq(128) expect(bulb.brightness_in_unit_interval).to eq(0.5019607843137255) expect(bulb.brightness_percent).to eq(50) with_fake_update('lights/1/state', bri: 128) - bulb.brightness = "128" + bulb.brightness = '128' expect(bulb.brightness).to eq(128) end - it 'should allow setting blink, solid and flash alerts' do + it 'allows setting blink, solid and flash alerts' do with_fake_update('lights/1/state', alert: 'lselect') bulb.blink expect(bulb.blinking?).to be true @@ -125,7 +125,7 @@ describe Hue::Bulb do end end - it 'should allow setting colorloop, and effect' do + it 'allows setting colorloop, and effect' do with_fake_update('lights/1/state', effect: 'new') bulb.effect = 'new' expect(bulb.effect).to eq('new') @@ -146,15 +146,12 @@ describe Hue::Bulb do expect(bulb.effect).to eq('colorloop') end - it 'should allow setting the transitions time, and employ it for a state change' do + it 'allows setting the transitions time, and employ it for a state change' do bulb.transition_time = 10 with_fake_update('lights/1/state', transitiontime: 100, bri: 255) bulb.brightness = 255 end - end - end - end diff --git a/hue/spec/hue/cli/command_spec.rb b/hue/spec/hue/cli/command_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Hue::CLI::Command do + before do + mock_application_config_path + mock_bridge_config_path + allow(Hue::CLI).to receive(:bridge).and_return(test_bridge) + end + + let(:subclass) do + Class.new(described_class) do + def self.name + 'Hue::CLI::Commands::Test' + end + + def hello(*_args) + :hi + end + end + end + + it 'exposes the configured bridge' do + expect(described_class.new.bridge).to be_a(Hue::Bridge) + end + + it 'dispatches to a defined method' do + expect(subclass.new.send(:send_method, 'hello')).to eq(:hi) + end + + it 'raises Hue::CLI::Error for an unavailable method' do + expect { subclass.new.send(:send_method, 'nope') } + .to raise_error(Hue::CLI::Error, /not available/) + end + + it 'falls back to bridge.print_state when execute called with no args' do + instance = described_class.new + expect(instance.bridge).to receive(:print_state) + instance.execute + end +end diff --git a/hue/spec/hue/cli/commands/delete_spec.rb b/hue/spec/hue/cli/commands/delete_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Hue::CLI::Commands::Delete do + before do + mock_application_config_path + mock_bridge_config_path + allow(Hue::CLI).to receive(:bridge).and_return(test_bridge) + end + + let(:command) { described_class.new } + + it 'unregisters the current bridge when called with no args' do + with_fake_delete("config/whitelist/#{TEST_APPLICATION_UUID}") + command.execute + end + + it 'unregisters a specific application id when given' do + other_id = 'other_app' + stub_request(:delete, "#{TEST_BRIDGE_URI}/#{other_id}/config/whitelist/#{other_id}") + .to_return(body: '[{}]', status: 200, headers: { 'Content-Type' => 'application/json' }) + command.execute(other_id) + end +end diff --git a/hue/spec/hue/cli/commands/light_spec.rb b/hue/spec/hue/cli/commands/light_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Hue::CLI::Commands::Light do + before do + mock_application_config_path + mock_bridge_config_path + allow(Hue::CLI).to receive(:bridge).and_return(test_bridge) + with_fake_request('lights/1') + end + + let(:command) { described_class.new } + + it 'rejects non-numeric ids' do + expect { command.execute('xyz') } + .to raise_error(Hue::CLI::Error, /Not a valid light number/) + end + + it 'prints state when only an id is given' do + out = with_stdout { command.execute('1') } + expect(out).to include('Living') + end + + it 'turns the bulb on' do + with_fake_update('lights/1/state', on: true) + command.execute('1', 'on') + end + + it 'turns the bulb off' do + with_fake_update('lights/1/state', on: false) + command.execute('1', 'off') + end + + it 'sets brightness' do + with_fake_update('lights/1/state', bri: 200) + command.execute('1', 'brightness', '200') + end + + it 'sets color' do + with_fake_update('lights/1/state', hue: 30_000, sat: 200) + command.execute('1', 'color', '30000', '200') + end + + it 'sets the name' do + with_fake_update('lights/1', name: 'Kitchen') + command.execute('1', 'name', 'Kitchen') + end + + it 'starts a colorloop effect' do + with_fake_update('lights/1/state', effect: 'colorloop') + command.execute('1', 'colorloop') + end + + it 'clears effect and alert with bare clear' do + with_fake_update('lights/1/state', effect: 'none') + with_fake_update('lights/1/state', alert: 'none') + command.execute('1', 'clear') + end + + it 'dumps state as JSON' do + out = with_stdout { command.execute('1', 'dump') } + expect(JSON.parse(out)).to be_a(Hash) + end +end diff --git a/hue/spec/hue/cli/commands/lights_spec.rb b/hue/spec/hue/cli/commands/lights_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Hue::CLI::Commands::Lights do + before do + mock_application_config_path + mock_bridge_config_path + allow(Hue::CLI).to receive(:bridge).and_return(test_bridge) + end + + let(:command) { described_class.new } + + it 'prints all bulbs when called with no args' do + expect(command.bridge).to receive(:print_bulbs) + command.execute + end + + it 'invokes find when "find" is passed' do + expect(command.bridge).to receive(:add_lights) + command.execute('find') + end + + it 'applies an unknown action to every light' do + with_fake_request(:lights) + expect(Hue::CLI::Commands::Light).to receive(:new).exactly(3).times.and_return( + instance_double(Hue::CLI::Commands::Light, execute: nil) + ) + command.execute('on') + end +end diff --git a/hue/spec/hue/cli_spec.rb b/hue/spec/hue/cli_spec.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Hue::CLI do + before do + mock_application_config_path + mock_bridge_config_path + allow(Hue.logger).to receive(:warn) + described_class.retry_count = 0 + end + + describe '.run' do + it 'prints default state when called with no args' do + with_fake_request + with_fake_request(:lights) + out = with_stdout { described_class.run([]) } + expect(out).to include('Philips hue') + end + + it 'dispatches a known command by name' do + with_fake_request(:lights) + stub_request(:get, %r{#{TEST_BRIDGE_URI}/#{TEST_APPLICATION_UUID}/lights/\d+}) + .to_return(body: File.read(File.join(TestHelpers::JSON_FIXTURE_DIR, 'lights/1.json')), + status: 200, headers: { 'Content-Type' => 'application/json' }) + out = with_stdout { described_class.run(%w[lights]) } + expect(out).to include('Living') + end + + it 'dispatches a numeric arg to the Light command' do + with_fake_request('lights/1') + out = with_stdout { described_class.run(%w[1]) } + expect(out).to include('Living') + end + + it 'reports unknown commands' do + out = with_stdout { described_class.run(%w[bogus]) } + expect(out).to include('Unknown command: bogus') + end + + it 'shows registration instructions when not registered' do + stub_request(:any, /.*/).to_return(status: 200, body: '{}') + allow(Hue::Config::Application).to receive(:default) + .and_raise(Hue::Config::NotFound.new('not found')) + mock_udp_replies + out = with_stdout { described_class.run([]) } + expect(out).to include('Application not registered.') + end + + it 'gives up after several retries on bridge connection problems' do + fake_bridge = instance_double(Hue::Bridge, print_config: nil) + allow(described_class).to receive(:bridge).and_return(fake_bridge) + allow(fake_bridge).to receive(:print_state).and_raise(Hue::Error, 'execution expired') + allow(Hue).to receive(:register_bridges).and_return({}) + out = with_stdout { described_class.run([]) } + expect(out).to include('Giving up contacting bridge') + end + end +end diff --git a/hue/spec/hue/colors/color_spec.rb b/hue/spec/hue/colors/color_spec.rb @@ -1,17 +1,18 @@ -require 'spec_helper.rb' +# frozen_string_literal: true -describe Hue::Colors::Color do +require 'spec_helper' +describe Hue::Colors::Color do context 'implements a ranged method: max > value > min' do - it 'should return the min when value < min' do + it 'returns the min when value < min' do expect(described_class.ranged(0, -1, 2)).to eq(0) end - it 'should return the max when value > max' do + it 'returns the max when value > max' do expect(described_class.ranged(0, 3, 2)).to eq(2) end - it 'should return the value when max > value > min' do + it 'returns the value when max > value > min' do expect(described_class.ranged(0, 1, 2)).to eq(1) end end @@ -19,9 +20,9 @@ describe Hue::Colors::Color do def abstract_method(method) abstract_color = described_class.new expect(abstract_color).to respond_to(method) - expect { + expect do abstract_color.send(method) - }.to raise_error(described_class::ERROR_METHOD_NOT_IMPLEMENTED) + end.to raise_error(described_class::ERROR_METHOD_NOT_IMPLEMENTED) end context 'defines but does not implement methods:' do @@ -29,5 +30,4 @@ describe Hue::Colors::Color do it('#to_s') { abstract_method(:to_s) } it('#to_rgb') { abstract_method(:to_rgb) } end - end diff --git a/hue/spec/hue/colors/color_temperature_spec.rb b/hue/spec/hue/colors/color_temperature_spec.rb @@ -1,70 +1,69 @@ -# Encoding: UTF-8 -require 'spec_helper.rb' +# frozen_string_literal: true -describe Hue::Colors::ColorTemperature do +require 'spec_helper' +describe Hue::Colors::ColorTemperature do context 'when initialized with a valid value' do let(:color) { described_class.new(500) } - it 'should report the temperature in mireds and kelvins' do + it 'reports the temperature in mireds and kelvins' do expect(color.mired).to eq(500) expect(color.kelvin).to eq(2000) end - it 'should have a string representation' do - expect(color.to_s).to eq("Temperature=2000°K (500 mired)") + it 'has a string representation' do + expect(color.to_s).to eq('Temperature=2000°K (500 mired)') end - it 'should have a hash representation' do - expect(color.to_hash).to eq({ct: 500}) + it 'has a hash representation' do + expect(color.to_hash).to eq({ ct: 500 }) end - it 'should have an RGB representation' do - expect(color.to_rgb).to eq(Hue::Colors::RGB.new(255,136,13)) + it 'has an RGB representation' do + expect(color.to_rgb).to eq(Hue::Colors::RGB.new(255, 136, 13)) end context 'when allowing change to the temperature value' do - it 'should go to the max in kelvins' do + it 'goes to the max in kelvins' do color.kelvin = 7000 expect(color.kelvin).to eq(described_class::KELVIN_MAX) expect(color.mired).to eq(described_class::MIRED_MIN) end - it 'should go to the max in mireds' do + it 'goes to the max in mireds' do color.mired = 600 expect(color.mired).to eq(described_class::MIRED_MAX) expect(color.kelvin).to eq(described_class::KELVIN_MIN) end - it 'should go to the min in mireds' do + it 'goes to the min in mireds' do color.mired = 100 expect(color.mired).to eq(described_class::MIRED_MIN) expect(color.kelvin).to eq(described_class::KELVIN_MAX) end - it 'should go to the min in kelvins' do + it 'goes to the min in kelvins' do color.mired = 2000 expect(color.mired).to eq(described_class::MIRED_MAX) expect(color.kelvin).to eq(described_class::KELVIN_MIN) end - it 'should hit the middle' do - mired_middle = described_class::MIRED_MAX/2 + it 'hits the middle' do + mired_middle = described_class::MIRED_MAX / 2 color.mired = mired_middle expect(color.mired).to eq(mired_middle) expect(color.kelvin).to eq(4000) - kelvin_middle = described_class::KELVIN_MAX/2 + kelvin_middle = described_class::KELVIN_MAX / 2 color.kelvin = kelvin_middle expect(color.kelvin).to eq(kelvin_middle) expect(color.mired).to eq(307) end - end end context 'when initializing' do - it 'should set the temperature depending on the value' do + it 'sets the temperature depending on the value' do color = described_class.new(100) expect(color.mired).to eq(described_class::MIRED_MIN) @@ -78,29 +77,28 @@ describe Hue::Colors::ColorTemperature do expect(color.kelvin).to eq(described_class::KELVIN_MAX) end - it 'should report kelvins as an integer' do + it 'reports kelvins as an integer' do color = described_class.new(459) expect(color.kelvin).to eq(2179) end - it 'should accept a string value for the temperature' do - color = described_class.new("100") + it 'accepts a string value for the temperature' do + color = described_class.new('100') expect(color.mired).to eq(described_class::MIRED_MIN) color = described_class.new((described_class::KELVIN_MAX + 3000).to_s) expect(color.kelvin).to eq(described_class::KELVIN_MAX) end - it 'should allow a percentage value for the mired scale' do - color = described_class.new("0%") + it 'allows a percentage value for the mired scale' do + color = described_class.new('0%') expect(color.mired).to eq(described_class::MIRED_MIN) - color = described_class.new("50%") + color = described_class.new('50%') expect(color.mired).to eq(326) - color = described_class.new("100%") + color = described_class.new('100%') expect(color.mired).to eq(described_class::MIRED_MAX) end end - end diff --git a/hue/spec/hue/colors/hue_saturation_spec.rb b/hue/spec/hue/colors/hue_saturation_spec.rb @@ -1,59 +1,59 @@ -# Encoding: UTF-8 -require 'spec_helper.rb' +# frozen_string_literal: true -describe Hue::Colors::HueSaturation do +require 'spec_helper' +describe Hue::Colors::HueSaturation do context 'when initialized with valid values' do let(:color) { described_class.new(2e4, 122) } - it 'should report those values' do + it 'reports those values' do expect(color.hue).to eq(20_000) expect(color.saturation).to eq(122) end - it 'should report hue in degrees and a closed unit interval' do + it 'reports hue in degrees and a closed unit interval' do expect(color.hue_in_degrees).to eq(109.86328125) expect(color.hue_in_unit_interval).to eq(0.30517578125) end - it 'should report saturation in a closed unit interval' do + it 'reports saturation in a closed unit interval' do expect(color.saturation_in_unit_interval).to eq(0.47843137254901963) end - it 'should have a string representation' do - expect(color.to_s).to eq("Hue=20000, Saturation=122") + it 'has a string representation' do + expect(color.to_s).to eq('Hue=20000, Saturation=122') end - it 'should have a hash representation' do - expect(color.to_hash).to eq({hue: 20_000, sat: 122}) + it 'has a hash representation' do + expect(color.to_hash).to eq({ hue: 20_000, sat: 122 }) end - it 'should have an RGB representation' do - expect(color.to_rgb).to eq(Hue::Colors::RGB.new(153,255,132)) + it 'has an RGB representation' do + expect(color.to_rgb).to eq(Hue::Colors::RGB.new(153, 255, 132)) end context 'when allowing change to the color values' do - it 'should allow setting the maximum hue' do + it 'allows setting the maximum hue' do color.hue = 70_000 expect(color.hue).to eq(described_class::HUE_MAX) expect(color.hue_in_degrees).to eq(360) expect(color.hue_in_unit_interval).to eq(1.0) end - it 'should allow setting the minimum hue' do + it 'allows setting the minimum hue' do color.hue = -1000 expect(color.hue).to eq(described_class::HUE_MIN) expect(color.hue_in_degrees).to eq(0) expect(color.hue_in_unit_interval).to eq(0.0) end - it 'should allow setting the maximum saturation' do + it 'allows setting the maximum saturation' do color.saturation = 300 expect(color.sat).to eq(described_class::SATURATION_MAX) expect(color.sat_in_unit_interval).to eq(1.0) end - it 'should allow setting the minimum saturation' do + it 'allows setting the minimum saturation' do color.saturation = -1 expect(color.sat).to eq(described_class::SATURATION_MIN) expect(color.sat_in_unit_interval).to eq(0.0) @@ -61,31 +61,29 @@ describe Hue::Colors::HueSaturation do end context 'when initialized with other values' do - it 'should accept hue and saturation values' do + it 'accepts hue and saturation values' do color = described_class.new(3e4, 200) expect(color.hue).to eq(3e4) expect(color.sat).to eq(200) end - it 'should accept a percentage of the hue scale with saturation value' do + it 'accepts a percentage of the hue scale with saturation value' do color = described_class.new('20%', 150) - expect(color.hue).to eq(13107) + expect(color.hue).to eq(13_107) expect(color.sat).to eq(150) end - it 'should accept a hue value with a percentage of the saturation scale' do + it 'accepts a hue value with a percentage of the saturation scale' do color = described_class.new(7e4, '100%') expect(color.hue).to eq(Hue::Colors::HueSaturation::HUE_MAX) expect(color.sat).to eq(Hue::Colors::HueSaturation::SATURATION_MAX) end - it 'should accept hue and saturation strings' do - color = described_class.new("30_000", "1333") + it 'accepts hue and saturation strings' do + color = described_class.new('30_000', '1333') expect(color.hue).to eq(3e4) expect(color.sat).to eq(Hue::Colors::HueSaturation::SATURATION_MAX) end end - end - end diff --git a/hue/spec/hue/colors/rgb_spec.rb b/hue/spec/hue/colors/rgb_spec.rb @@ -1,31 +1,31 @@ -# Encoding: UTF-8 -require 'spec_helper.rb' +# frozen_string_literal: true -describe Hue::Colors::RGB do +require 'spec_helper' +describe Hue::Colors::RGB do context 'when initialized with a valid values' do let(:color) { described_class.new(64, 128, 248) } - it 'should report those values' do + it 'reports those values' do expect(color.red).to eq(64) expect(color.green).to eq(128) expect(color.blue).to eq(248) end - it 'should have a string representation' do - expect(color.to_s).to eq("RGB≈[64, 128, 248]") + it 'has a string representation' do + expect(color.to_s).to eq('RGB≈[64, 128, 248]') end - it 'should have a hash representation' do - expect(color.to_hash).to eq({hue: 21845, sat: 127, bri: 255 }) + it 'has a hash representation' do + expect(color.to_hash).to eq({ hue: 39_891, sat: 189, bri: 255 }) end - it 'should have an RGB representation' do + it 'has an RGB representation' do expect(color.to_rgb).to eq(color) end context 'when allowing change to the color values' do - it 'should allow setting the maximum' do + it 'allows setting the maximum' do color.red = 300 expect(color.red).to eq(described_class::MAX) color.green = 255 @@ -34,7 +34,7 @@ describe Hue::Colors::RGB do expect(color.red).to eq(described_class::MAX) end - it 'should allow setting the minimum' do + it 'allows setting the minimum' do color.red = 0 expect(color.red).to eq(described_class::MIN) color.green = -1 @@ -45,14 +45,14 @@ describe Hue::Colors::RGB do end context 'when initializing' do - it 'should allow strings' do + it 'allows strings' do color = described_class.new('200', '100', '50') expect(color.red).to eq(200) expect(color.green).to eq(100) expect(color.blue).to eq(50) end - it 'should allow percentages' do + it 'allows percentages' do color = described_class.new('10%', '100%', '50%') expect(color.red).to eq(26) expect(color.green).to eq(255) @@ -60,5 +60,4 @@ describe Hue::Colors::RGB do end end end - end diff --git a/hue/spec/hue/colors/xy_spec.rb b/hue/spec/hue/colors/xy_spec.rb @@ -1,37 +1,37 @@ -# Encoding: UTF-8 -require 'spec_helper.rb' +# frozen_string_literal: true -describe Hue::Colors::XY do +require 'spec_helper' +describe Hue::Colors::XY do context 'when initialized with a valid values' do let(:color) { described_class.new(0.5, 0.5) } - it 'should report those values' do + it 'reports those values' do expect(color.x).to eq(0.5) expect(color.y).to eq(0.5) end - it 'should have a string representation' do - expect(color.to_s).to eq("XY=[0.5, 0.5]") + it 'has a string representation' do + expect(color.to_s).to eq('XY=[0.5, 0.5]') end - it 'should have a hash representation' do - expect(color.to_hash).to eq({xy: [0.5, 0.5]}) + it 'has a hash representation' do + expect(color.to_hash).to eq({ xy: [0.5, 0.5] }) end - it 'should have an RGB representation' do - expect(color.to_rgb).to eq(Hue::Colors::RGB.new(217,209,41)) + it 'has an RGB representation' do + expect(color.to_rgb).to eq(Hue::Colors::RGB.new(217, 209, 41)) end context 'when allowing change to the color values' do - it 'should allow setting the maximum' do + it 'allows setting the maximum' do color.x = 1.1 expect(color.x).to eq(described_class::MAX) color.y = 2 expect(color.y).to eq(described_class::MAX) end - it 'should allow setting the minimum' do + it 'allows setting the minimum' do color.x = -1 expect(color.x).to eq(described_class::MIN) color.y = 0 @@ -40,12 +40,11 @@ describe Hue::Colors::XY do end context 'when initializing' do - it 'should allow strings' do + it 'allows strings' do color = described_class.new('0.22', '10.00') expect(color.x).to eq(0.22) expect(color.y).to eq(1.00) end end end - end diff --git a/hue/spec/hue/colors_spec.rb b/hue/spec/hue/colors_spec.rb @@ -1,38 +1,39 @@ -require 'spec_helper.rb' +# frozen_string_literal: true -describe Hue::Colors do +require 'spec_helper' +describe Hue::Colors do context 'when #parse_state is used' do it 'can return a ColorTemperature' do - color = described_class.parse_state({'colormode' => 'ct', 'ct' => 500}) + color = described_class.parse_state({ 'colormode' => 'ct', 'ct' => 500 }) expect(color).to be_a(Hue::Colors::ColorTemperature) expect(color.mired).to eq(500) end it 'can return HueSaturation' do - color = described_class.parse_state({'colormode' => 'hs', 'hue' => 30_000, 'sat' => 150}) + color = described_class.parse_state({ 'colormode' => 'hs', 'hue' => 30_000, 'sat' => 150 }) expect(color).to be_a(Hue::Colors::HueSaturation) expect(color.hue).to eq(30_000) expect(color.sat).to eq(150) end it 'can return XY' do - color = described_class.parse_state({'colormode' => 'xy', 'xy' => [0.8, 0.2]}) + color = described_class.parse_state({ 'colormode' => 'xy', 'xy' => [0.8, 0.2] }) expect(color).to be_a(Hue::Colors::XY) expect(color.x).to eq(0.8) expect(color.y).to eq(0.2) end it 'throws error if no mode is passed' do - expect { + expect do described_class.parse_state({}) - }.to raise_error(Hue::Error, /Unknown or missing state/) + end.to raise_error(Hue::Error, /Unknown or missing state/) end end context 'when #parse is used' do it 'can return a ColorTemperature with single value' do - color = described_class.parse("5000") + color = described_class.parse('5000') expect(color).to be_a(Hue::Colors::ColorTemperature) expect(color.kelvin).to eq(5000) @@ -40,43 +41,43 @@ describe Hue::Colors do expect(color).to be_a(Hue::Colors::ColorTemperature) expect(color.mired).to eq(Hue::Colors::ColorTemperature::MIRED_MIN) - color = described_class.parse("100%") + color = described_class.parse('100%') expect(color).to be_a(Hue::Colors::ColorTemperature) expect(color.mired).to eq(Hue::Colors::ColorTemperature::MIRED_MAX) end it 'can return HueSaturation' do - color = described_class.parse(*[3e4, 200]) + color = described_class.parse(3e4, 200) expect(color).to be_a(Hue::Colors::HueSaturation) expect(color.hue).to eq(3e4) expect(color.sat).to eq(200) - color = described_class.parse(*['20%', 150]) + color = described_class.parse('20%', 150) expect(color).to be_a(Hue::Colors::HueSaturation) - expect(color.hue).to eq(13107) + expect(color.hue).to eq(13_107) expect(color.sat).to eq(150) - color = described_class.parse(*[7e4, '100%']) + color = described_class.parse(7e4, '100%') expect(color).to be_a(Hue::Colors::HueSaturation) expect(color.hue).to eq(Hue::Colors::HueSaturation::HUE_MAX) expect(color.sat).to eq(Hue::Colors::HueSaturation::SATURATION_MAX) end it 'can return XY' do - color = described_class.parse(*[0.2, 0.5]) + color = described_class.parse(0.2, 0.5) expect(color).to be_a(Hue::Colors::XY) expect(color.x).to eq(0.2) expect(color.y).to eq(0.5) end it 'can return RGB' do - color = described_class.parse(*[0.0, 300, 155]) + color = described_class.parse(0.0, 300, 155) expect(color).to be_a(Hue::Colors::RGB) expect(color.red).to eq(0) expect(color.green).to eq(255) expect(color.blue).to eq(155) - color = described_class.parse(*['10%', '100%', '50%']) + color = described_class.parse('10%', '100%', '50%') expect(color).to be_a(Hue::Colors::RGB) expect(color.red).to eq(26) expect(color.green).to eq(255) @@ -84,9 +85,9 @@ describe Hue::Colors do end it 'throws error if no mode is passed' do - expect { + expect do described_class.parse - }.to raise_error(Hue::Error, /Unable to parse to color:/) + end.to raise_error(Hue::Error, /Unable to parse to color:/) end end end diff --git a/hue/spec/hue/config/abstract_spec.rb b/hue/spec/hue/config/abstract_spec.rb @@ -1,14 +1,20 @@ -require 'spec_helper.rb' +# frozen_string_literal: true -class Hue::Config::AbstractTest < Hue::Config::Abstract - private - def add_self_to_yaml(yaml) - yaml[name] = { 1 => :test} +require 'spec_helper' + +module Hue + module Config + class AbstractTest < Hue::Config::Abstract + private + + def add_self_to_yaml(yaml) + yaml[name] = { 1 => :test } + end + end end end describe Hue::Config::Abstract do - before(:all) do create_test_application_config end @@ -16,17 +22,17 @@ describe Hue::Config::Abstract do context 'given an new config' do let(:config) { described_class.new('test', TEST_CONFIG_APPLICATION_PATH) } - it 'should report the values' do + it 'reports the values' do expect(config.name).to eq('test') expect(config.path).to eq(TEST_CONFIG_APPLICATION_PATH) end - it 'should allow writing the new config to file' do + it 'allows writing the new config to file' do config.write expect(YAML.load_file(config.path)['test']).to be_a(Hash) end - it 'should allow deleting that named config from the file' do + it 'allows deleting that named config from the file' do config.delete expect(YAML.load_file(config.path)['test']).to be_nil end @@ -35,11 +41,10 @@ describe Hue::Config::Abstract do context 'given an existing config' do let(:config) { described_class.new('test', EMPTY_CONFIG_FILE) } - it 'should allow writing to the file' do + it 'allows writing to the file' do config.write expect(YAML.load_file(config.path)['test']).to be_a(Hash) config.delete end end - end diff --git a/hue/spec/hue/config/application_spec.rb b/hue/spec/hue/config/application_spec.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true + require 'spec_helper' describe Hue::Config::Application do - - before(:each) do + before do mock_application_config_path end @@ -10,26 +11,26 @@ describe Hue::Config::Application do create_test_application_config end - it 'should report the config file location' do + it 'reports the config file location' do expect(described_class.file_path).to eq(TEST_CONFIG_APPLICATION_PATH) end - it "should throw and error if a named config doesn't exist" do - expect { + it "throws and error if a named config doesn't exist" do + expect do described_class.named('not_default') - }.to raise_error(Hue::Config::NotFound, /Config named (.*) not found/) + end.to raise_error(Hue::Config::NotFound, /Config named (.*) not found/) end context 'with a config file, containing a default' do let(:config) { described_class.default } - it "should give the default config and report it's values" do + it "gives the default config and report it's values" do expect(config.name).to eq(described_class::STRING_DEFAULT) expect(config.bridge_id).to eq(TEST_CONFIG_APPLICATION[config.name][described_class::STRING_BRIDGE_ID]) expect(config.id).to eq(TEST_CONFIG_APPLICATION[config.name][described_class::STRING_ID]) end - it 'should allow deleting the default config from the file' do + it 'allows deleting the default config from the file' do config.delete expect(YAML.load_file(described_class.file_path)[described_class::STRING_DEFAULT]).to be_nil end @@ -38,37 +39,36 @@ describe Hue::Config::Application do context 'given an new config' do let(:config) { described_class.new('http://someip/api', 'some_id', 'not_default') } - it 'should report the values' do + it 'reports the values' do expect(config.name).to eq('not_default') expect(config.bridge_id).to eq('http://someip/api') expect(config.id).to eq('some_id') end - it 'should allow writing the new config to file' do + it 'allows writing the new config to file' do config.write expect(YAML.load_file(described_class.file_path)['not_default']).to be_a(Hash) end - it 'should allow fetching that name config' do + it 'allows fetching that name config' do + config.write named_config = described_class.named('not_default') expect(named_config).to eq(config) end - it 'should allow deleting that named config from the file' do + it 'allows deleting that named config from the file' do config.delete expect(YAML.load_file(described_class.file_path)['not_default']).to be_nil end end context 'given an non existing config' do - - it "should throw and error if a named config doesn't exist" do + it "throws and error if a named config doesn't exist" do expect(described_class).to receive(:file_path).and_return(EMPTY_CONFIG_FILE).once - expect { + expect do described_class.named('not_default') - }.to raise_error(Hue::Config::NotFound, /Config named (.*) not found/) + end.to raise_error(Hue::Config::NotFound, /Config named (.*) not found/) end end - end diff --git a/hue/spec/hue/config/bridge_spec.rb b/hue/spec/hue/config/bridge_spec.rb @@ -1,8 +1,9 @@ -require 'spec_helper.rb' +# frozen_string_literal: true -describe Hue::Config::Bridge do +require 'spec_helper' - before(:each) do +describe Hue::Config::Bridge do + before do mock_bridge_config_path end @@ -10,11 +11,11 @@ describe Hue::Config::Bridge do create_test_bridge_config end - it 'should report the config file location' do + it 'reports the config file location' do expect(described_class.file_path).to eq(TEST_CONFIG_BRIDGE_PATH) end - it 'should find existing bridges given an id' do + it 'finds existing bridges given an id' do found = described_class.find(TEST_BRIDGE_UUID) expect(found.id).to eq(TEST_BRIDGE_UUID) expect(found.uri).to eq(TEST_BRIDGE_URI) @@ -27,11 +28,10 @@ describe Hue::Config::Bridge do let(:uri) { 'http://someip/api' } let(:config) { described_class.new(uuid, uri) } - it 'should report the values' do + it 'reports the values' do expect(config.name).to eq(uuid) expect(config.id).to eq(uuid) expect(config.uri).to eq(uri) end end - end diff --git a/hue/spec/hue/extensions/bridge_spec.rb b/hue/spec/hue/extensions/bridge_spec.rb @@ -0,0 +1,58 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Hue::Bridge, 'output extensions' do + let(:bridge) { test_bridge } + + before { Indentation.reset } + + describe '#print_config' do + it 'prints application id and bridge URI' do + out = with_stdout { bridge.print_config } + expect(out).to include('Config:') + expect(out).to include("Application id: #{TEST_APPLICATION_UUID}") + expect(out).to include("Bridge URI: #{TEST_BRIDGE_URI}") + end + end + + describe '#print_state' do + it 'prints basic info, network config, applications and lights' do + with_fake_request + out = with_stdout { bridge.print_state } + expect(out).to include('Philips hue') + expect(out).to include('Button pressed:') + expect(out).to include('Network configuration:') + expect(out).to include('IP:') + expect(out).to include('Applications:') + expect(out).to include('Lights:') + expect(out).to include('Living') + expect(out).to include('Bedroom Far') + end + + it 'prints proxy info when proxyaddress is set' do + proxy_state = JSON.parse(File.read(File.join(TestHelpers::JSON_FIXTURE_DIR, 'get_success.json'))) + proxy_state['config']['proxyaddress'] = 'proxy.example' + proxy_state['config']['proxyport'] = 8080 + stub_request(:get, "#{TEST_BRIDGE_URI}/#{TEST_APPLICATION_UUID}") + .to_return(body: proxy_state.to_json, status: 200, + headers: { 'Content-Type' => 'application/json' }) + + out = with_stdout { bridge.print_state } + expect(out).to include('Proxy: proxy.example:8080') + end + end + + describe '#print_bulbs' do + it 'iterates and prints each bulb' do + with_fake_request(:lights) + stub_request(:get, %r{#{TEST_BRIDGE_URI}/#{TEST_APPLICATION_UUID}/lights/\d+}) + .to_return(body: File.read(File.join(TestHelpers::JSON_FIXTURE_DIR, 'lights/1.json')), + status: 200, headers: { 'Content-Type' => 'application/json' }) + + out = with_stdout { bridge.print_bulbs } + expect(out).to include('Living') + expect(out).to include('Brightness:') + end + end +end diff --git a/hue/spec/hue/extensions/bulb_spec.rb b/hue/spec/hue/extensions/bulb_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Hue::Bulb, 'output extensions' do + let(:bulb) { described_class.new(test_bridge, 1) } + + before do + Indentation.reset + with_fake_request('lights/1') + bulb.refresh! + end + + describe '#print_state' do + it 'prints the bulb id, name and OFF status' do + out = with_stdout { bulb.print_state } + expect(out).to include('1.') + expect(out).to include('Living') + expect(out).to include('OFF') + expect(out).to include('Brightness:') + end + + it 'prints color when hue is present' do + out = with_stdout { bulb.print_state } + expect(out).to include('Color:') + end + + it 'prints transitory state when blinking' do + with_fake_update('lights/1/state', alert: 'lselect') + bulb.blink + out = with_stdout { bulb.print_state } + expect(out).to include('Blinking!') + end + + it 'prints transitory state when an effect is set' do + with_fake_update('lights/1/state', effect: 'colorloop') + bulb.colorloop + out = with_stdout { bulb.print_state } + expect(out).to include('Effect:') + end + + it 'prints ON when bulb is on' do + with_fake_update('lights/1/state', on: true) + bulb.on + out = with_stdout { bulb.print_state } + expect(out).to include('ON') + end + end +end diff --git a/hue/spec/hue/utilities/indentation_spec.rb b/hue/spec/hue/utilities/indentation_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Indentation do + let(:host) { Class.new { include Indentation }.new } + + before { described_class.reset } + + it 'starts at level 0 with no size' do + expect(described_class.indentation_level).to eq(0) + expect(described_class.indentation_size).to eq(0) + end + + it 'indents inside a block and restores on exit' do + with_stdout do + host.puts 'a' + host.indent(2) { host.puts 'b' } + host.puts 'c' + end.tap do |out| + expect(out).to eq("a\n b\nc\n") + end + end + + it 'restores level even if the block raises' do + expect { host.indent(2) { raise 'boom' } }.to raise_error('boom') + expect(described_class.indentation_level).to eq(0) + end + + it 'nests indentation' do + out = with_stdout do + host.indent(2) do + host.puts 'x' + host.indent(2) { host.puts 'y' } + end + end + expect(out).to eq(" x\n y\n") + end +end diff --git a/hue/spec/hue_spec.rb b/hue/spec/hue_spec.rb @@ -1,36 +1,35 @@ -require 'spec_helper.rb' +# frozen_string_literal: true + +require 'spec_helper' describe Hue do - before(:each) do + before do mock_application_config_path mock_bridge_config_path + mock_udp_replies end - after(:each) do + after do create_test_bridge_config end - before(:each) do - mock_udp_replies - end - - it 'should report the device type as itself' do + it 'reports the device type as itself' do expect(described_class.device_type).to eq(described_class::DEVICE_TYPE) end - it 'should return the default application' do + it 'returns the default application' do expect(described_class.application).to be_a(described_class::Bridge) end context 'when discovering a bridge on the network' do - it 'should return a list discovered bridges' do - bridges = Hue.discover - expect(bridges).to eq({TEST_UDP_BRIDGE_UUID => TEST_UDP_BRIDGE_URI}) + it 'returns a list discovered bridges' do + bridges = described_class.discover + expect(bridges).to eq({ TEST_UDP_BRIDGE_UUID => TEST_UDP_BRIDGE_URI }) end - it 'should allow registering of discovered bridges' do + it 'allows registering of discovered bridges' do expect(Hue::Config::Bridge.find(TEST_UDP_BRIDGE_UUID)).to be_nil - registered = Hue.register_bridges + registered = described_class.register_bridges new_bridge = registered[TEST_UDP_BRIDGE_UUID] expect(new_bridge.id).to eq(TEST_UDP_BRIDGE_UUID) expect(new_bridge.uri).to eq(TEST_UDP_BRIDGE_URI) @@ -38,27 +37,27 @@ describe Hue do end context 'when attempting discover without a bridge on the network' do - before(:each) do + before do mock_udp_no_reply mock_nupnp_empty_reply end - it 'should return an empty list of bridges' do - bridges = Hue.discover + it 'returns an empty list of bridges' do + bridges = described_class.discover expect(bridges).to eq({}) end end context 'after discovering bridges' do - before(:each) do + before do mock_udp_replies(TEST_BRIDGE_UUID, 'new_host') end - it 'should update already registered bridges' do + it 'updates already registered bridges' do bridge = Hue::Config::Bridge.find(TEST_BRIDGE_UUID) expect(bridge).not_to be_nil - registered = Hue.register_bridges + registered = described_class.register_bridges updated_bridge = registered[TEST_BRIDGE_UUID] expect(updated_bridge.id).to eq(TEST_BRIDGE_UUID) @@ -67,13 +66,13 @@ describe Hue do end context 'when registering or un-registering the application' do - it 'should throw and error if the default already exists' do - expect { + it 'throws and error if the default already exists' do + expect do described_class.register_default - }.to raise_error(described_class::Error, described_class::ERROR_DEFAULT_EXISTS) + end.to raise_error(described_class::Error, described_class::ERROR_DEFAULT_EXISTS) end - it 'should allow a new default if one doesn\'t exist' do + it "allows a new default if one doesn't exist" do with_temp_config_path do with_fake_post(nil, {}, 'post_success', TEST_UDP_BRIDGE_URI) with_stdout(/Registering new app...$/) do @@ -82,7 +81,7 @@ describe Hue do end end - it 'should allow un-registering the default' do + it 'allows un-registering the default' do with_temp_config_path(true) do with_fake_delete("config/whitelist/#{TEST_APPLICATION_UUID}") described_class.remove_default @@ -97,5 +96,4 @@ describe Hue do expect(described_class.percent_to_unit_interval('100%')).to eq(1.00) end end - end diff --git a/hue/spec/spec_helper.rb b/hue/spec/spec_helper.rb @@ -1,11 +1,22 @@ # frozen_string_literal: true +require 'simplecov' +SimpleCov.start do + add_filter '/spec/' + add_filter '/pkg/' + add_filter '/vendor/' +end + require 'hue' require 'rspec' require 'webmock/rspec' +require_relative 'test_helpers' + +include TestHelpers # rubocop:disable Style/MixinUsage -- expose constants at top-level for specs RSpec.configure do |config| config.mock_with :rspec + config.include TestHelpers end WebMock.disable_net_connect! diff --git a/hue/spec/test_helpers.rb b/hue/spec/test_helpers.rb @@ -0,0 +1,150 @@ +# frozen_string_literal: true + +require 'fileutils' +require 'json' +require 'stringio' +require 'tmpdir' +require 'yaml' + +module TestHelpers + TEST_BRIDGE_UUID = 'bc6be180-4c57-0130-8d8f-0018de9ecdd0' + TEST_BRIDGE_URI = 'http://localhost/api' + TEST_APPLICATION_UUID = 'application_uuid' + + TEST_UDP_BRIDGE_UUID = 'aa0be180-4c57-0130-8d8f-0018de9ecdd0' + TEST_UDP_BRIDGE_IP = '10.0.0.42' + TEST_UDP_BRIDGE_URI = "http://#{TEST_UDP_BRIDGE_IP}/api".freeze + + FIXTURE_DIR = File.expand_path('config', __dir__) + JSON_FIXTURE_DIR = File.expand_path('json', __dir__) + EMPTY_CONFIG_FILE = File.join(FIXTURE_DIR, 'empty.yml') + + TEST_TMP_DIR = File.join(Dir.tmpdir, 'hue-spec') + TEST_CONFIG_APPLICATION_PATH = File.join(TEST_TMP_DIR, 'applications.yml') + TEST_CONFIG_BRIDGE_PATH = File.join(TEST_TMP_DIR, 'bridges.yml') + + TEST_CONFIG_APPLICATION = YAML.load_file(File.join(FIXTURE_DIR, 'applications.yml')) + TEST_CONFIG_BRIDGE = YAML.load_file(File.join(FIXTURE_DIR, 'bridges.yml')) + + def mock_application_config_path + FileUtils.mkdir_p(TEST_TMP_DIR) + create_test_application_config + allow(Hue::Config::Application).to receive(:file_path).and_return(TEST_CONFIG_APPLICATION_PATH) + end + + def mock_bridge_config_path + FileUtils.mkdir_p(TEST_TMP_DIR) + create_test_bridge_config + allow(Hue::Config::Bridge).to receive(:file_path).and_return(TEST_CONFIG_BRIDGE_PATH) + end + + def create_test_application_config + FileUtils.mkdir_p(TEST_TMP_DIR) + File.write(TEST_CONFIG_APPLICATION_PATH, YAML.dump(TEST_CONFIG_APPLICATION)) + end + + def create_test_bridge_config + FileUtils.mkdir_p(TEST_TMP_DIR) + File.write(TEST_CONFIG_BRIDGE_PATH, YAML.dump(TEST_CONFIG_BRIDGE)) + end + + def mock_udp_replies(uuid = TEST_UDP_BRIDGE_UUID, ip = TEST_UDP_BRIDGE_IP) + fake_socket = instance_double(UDPSocket, send: 0) + payload = "HTTP/1.1 200 OK\r\nLOCATION: http://#{ip}/description.xml\r\n" \ + "ST: urn:schemas-upnp-org:device:Basic:1\r\nUSN: uuid:#{uuid}::IpBridge\r\n\r\n" + delivered = false + allow(fake_socket).to receive(:recvfrom) do + raise Hue::Error, 'simulated timeout' if delivered + + delivered = true + [payload, [nil, 1900, 'fake.local', ip]] + end + allow(UDPSocket).to receive(:new).and_return(fake_socket) + end + + def mock_udp_no_reply + fake_socket = instance_double(UDPSocket, send: 0) + allow(fake_socket).to receive(:recvfrom) { raise Hue::Error, 'simulated timeout' } + allow(UDPSocket).to receive(:new).and_return(fake_socket) + end + + def mock_nupnp_empty_reply + stub_request(:get, 'https://www.meethue.com/api/nupnp').to_return(body: '[]', status: 200) + end + + def mock_socket_hostname + allow(Socket).to receive(:gethostname).and_return('hostname') + end + + def with_fake_request(path = nil, kind = :get_success) + fixture = kind == :get_success ? (path || :get_success) : kind + stub_request(:get, bridge_request_url(TEST_BRIDGE_URI, path)) + .to_return(body: read_json_fixture(fixture), status: 200, + headers: { 'Content-Type' => 'application/json' }) + end + + def with_fake_post(path = nil, body = nil, fixture = 'post_success', host = TEST_BRIDGE_URI) + stub = stub_request(:post, raw_request_url(host, path)) + stub = stub.with(body: body) if body + stub.to_return(body: read_json_fixture(fixture), status: 200, + headers: { 'Content-Type' => 'application/json' }) + end + + def with_fake_delete(path = nil, fixture = 'put_success') + stub_request(:delete, bridge_request_url(TEST_BRIDGE_URI, path)) + .to_return(body: read_json_fixture(fixture), status: 200, + headers: { 'Content-Type' => 'application/json' }) + end + + def with_fake_update(path = nil, body = nil, fixture = 'put_success') + stub = stub_request(:put, bridge_request_url(TEST_BRIDGE_URI, path)) + stub = stub.with(body: body) if body + stub.to_return(body: read_json_fixture(fixture), status: 200, + headers: { 'Content-Type' => 'application/json' }) + end + + def api_reply(name) + JSON.parse(read_json_fixture(name)) + end + + def with_temp_config_path(with_default = false) + File.write(TEST_CONFIG_APPLICATION_PATH, YAML.dump(with_default ? TEST_CONFIG_APPLICATION : {})) + yield + ensure + create_test_application_config + end + + def with_stdout(matcher = nil) + original = $stdout + $stdout = StringIO.new + yield + output = $stdout.string + expect(output).to match(matcher) if matcher + output + ensure + $stdout = original + end + + def test_bridge + Hue::Bridge.new(TEST_APPLICATION_UUID, TEST_BRIDGE_URI) + end + + private + + def read_json_fixture(name) + File.read(File.join(JSON_FIXTURE_DIR, "#{name}.json")) + end + + def bridge_request_url(host, path) + base = host.sub(%r{/$}, '') + segments = [base] + segments << TEST_APPLICATION_UUID unless path.to_s.start_with?(TEST_APPLICATION_UUID) + segments << path.to_s if path && !path.to_s.empty? + segments.reject { |s| s.to_s.empty? }.join('/') + end + + def raw_request_url(host, path) + base = host.sub(%r{/$}, '') + path && !path.to_s.empty? ? "#{base}/#{path}" : base + end +end