Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **`MinerPool#load_miners!`** now raises `CgminerApiClient::Error`
(was `RuntimeError`) when `config/miners.yml` is missing. Existing
`rescue StandardError` clauses still work.
- Test-support code (FakeCgminer, CgminerFixtures) extracted to the
shared `cgminer_test_support` gem. Spec references now use
`CgminerTestSupport::FakeCgminer` and
`CgminerTestSupport::Fixtures::SUMMARY` etc. `script/fake_cgminer`
is now a thin shim that delegates to `bundle exec fake_cgminer`;
operator muscle memory unchanged.

### Fixed
- **`MinerPool` no longer silently defaults a miners.yml entry
Expand Down
4 changes: 4 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ gemspec

group :development do
gem 'bundler-audit', '>= 0.9'
gem 'cgminer_test_support',
git: 'https://github.com/jramos/cgminer_test_support.git',
tag: 'v0.1.0',
require: false
gem 'rake', '>= 13.2'
gem 'rspec', '>= 3.13'
gem 'rubocop', '>= 1.60'
Expand Down
45 changes: 7 additions & 38 deletions script/fake_cgminer
Original file line number Diff line number Diff line change
@@ -1,41 +1,10 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

# Spin up a fake cgminer for manual testing. Reuses the same
# FakeCgminer class the integration specs use, so behavior is
# identical to what tests verify.
#
# Usage:
# ./script/fake_cgminer # listen on 127.0.0.1:4028
# ./script/fake_cgminer 5000 # listen on 127.0.0.1:5000
#
# Then in another terminal, point config/miners.yml at that
# host/port and run the CLI:
#
# cp config/miners.yml.example config/miners.yml
# bundle exec bin/cgminer_api_client summary
#
# This file lives in script/ rather than bin/ deliberately: the
# gemspec packages bin/* but not script/*, so this stays a
# developer tool and is not shipped to gem consumers.

$stdout.sync = true
$LOAD_PATH.unshift File.expand_path('../spec/support', __dir__)
require 'json'
require 'socket'
require 'cgminer_fixtures'
require 'fake_cgminer'

port = (ARGV[0] || 4028).to_i
server = FakeCgminer.new(port: port).start

puts "fake cgminer listening on 127.0.0.1:#{server.port}"
puts "commands available: #{CgminerFixtures::DEFAULT.keys.sort.join(', ')}"
puts 'press Ctrl-C to stop'

trap('INT') do
server.stop
exit 0
end

sleep
# Thin shim. The real runner now lives in cgminer_test_support's
# exe/fake_cgminer. Kept at this path so historical operator muscle
# memory ('./script/fake_cgminer 4028') keeps working from any CWD —
# we Dir.chdir into the api_client repo root first so Bundler can
# resolve the dev-group git+tag dependency.
Dir.chdir(File.expand_path('..', __dir__))
exec 'bundle', 'exec', 'fake_cgminer', *ARGV
18 changes: 9 additions & 9 deletions spec/integration/cli_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
require 'tmpdir'

# End-to-end tests for bin/cgminer_api_client. Spawns the real
# binary via Open3 against a live FakeCgminer on an ephemeral
# binary via Open3 against a live CgminerTestSupport::FakeCgminer on an ephemeral
# port, with a temporary config/miners.yml written to a tmpdir so
# the CLI's MinerPool.new finds it. Asserts on exit code,
# stdout/stderr split, and DEBUG=1 backtrace behavior.
Expand All @@ -30,7 +30,7 @@ def run_cli(args, miners:, env: {})

describe 'happy path: single miner, successful query' do
it 'prints the summary on stdout with a host:port header and exits 0' do
FakeCgminer.with do |port|
CgminerTestSupport::FakeCgminer.with do |port|
stdout, stderr, status = run_cli(
%w[summary],
miners: [{ host: '127.0.0.1', port: port }]
Expand Down Expand Up @@ -87,7 +87,7 @@ def run_cli(args, miners:, env: {})
closed_port = dummy.addr[1]
dummy.close

FakeCgminer.with do |good_port|
CgminerTestSupport::FakeCgminer.with do |good_port|
stdout, stderr, status = run_cli(
%w[summary],
miners: [
Expand Down Expand Up @@ -133,7 +133,7 @@ def run_cli(args, miners:, env: {})

describe '-v / --verbose flag' do
it 'logs >>> / <<< lines with host:port prefix when -v precedes the command' do
FakeCgminer.with do |port|
CgminerTestSupport::FakeCgminer.with do |port|
stdout, stderr, status = run_cli(
['-v', 'summary'],
miners: [{ host: '127.0.0.1', port: port }]
Expand All @@ -149,7 +149,7 @@ def run_cli(args, miners:, env: {})
end

it 'accepts --verbose as the long form' do
FakeCgminer.with do |port|
CgminerTestSupport::FakeCgminer.with do |port|
_stdout, stderr, status = run_cli(
['--verbose', 'summary'],
miners: [{ host: '127.0.0.1', port: port }]
Expand All @@ -162,7 +162,7 @@ def run_cli(args, miners:, env: {})
end

it 'permutes: the flag still works when it follows the command' do
FakeCgminer.with do |port|
CgminerTestSupport::FakeCgminer.with do |port|
_stdout, stderr, status = run_cli(
['summary', '-v'],
miners: [{ host: '127.0.0.1', port: port }]
Expand All @@ -174,7 +174,7 @@ def run_cli(args, miners:, env: {})
end

it 'is silent on stderr without the flag' do
FakeCgminer.with do |port|
CgminerTestSupport::FakeCgminer.with do |port|
_stdout, stderr, status = run_cli(
%w[summary],
miners: [{ host: '127.0.0.1', port: port }]
Expand All @@ -187,8 +187,8 @@ def run_cli(args, miners:, env: {})
end

it 'prefixes each line with the originating miner so the fan-out is grep-able' do
FakeCgminer.with do |a_port|
FakeCgminer.with do |b_port|
CgminerTestSupport::FakeCgminer.with do |a_port|
CgminerTestSupport::FakeCgminer.with do |b_port|
_stdout, stderr, status = run_cli(
['-v', 'summary'],
miners: [
Expand Down
32 changes: 16 additions & 16 deletions spec/integration/miner_integration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
require 'spec_helper'

# End-to-end integration tests: a real Miner instance talks to a real
# TCP socket served by FakeCgminer in a background thread. These
# TCP socket served by CgminerTestSupport::FakeCgminer in a background thread. These
# tests exercise the entire request → wire → response → parse →
# result path, which the mock-based unit tests can't. Every test
# here validates a distinct code path in Miner#perform_request or
Expand All @@ -15,7 +15,7 @@ def miner_at(port)

describe 'read-only commands' do
it 'returns a sanitized Hash for a single-result command (summary)' do
FakeCgminer.with do |port|
CgminerTestSupport::FakeCgminer.with do |port|
result = miner_at(port).summary
expect(result).to include(
mhs_av: 56_789.12,
Expand All @@ -27,7 +27,7 @@ def miner_at(port)
end

it 'returns an Array of sanitized Hashes for an array-result command (devs)' do
FakeCgminer.with do |port|
CgminerTestSupport::FakeCgminer.with do |port|
result = miner_at(port).devs
expect(result).to be_an(Array)
expect(result.size).to eq(2)
Expand All @@ -37,7 +37,7 @@ def miner_at(port)
end

it 'parses a multi-command response and iterates per-command statuses (summary+pools)' do
FakeCgminer.with do |port|
CgminerTestSupport::FakeCgminer.with do |port|
result = miner_at(port).query('summary+pools')
expect(result).to be_a(Hash)
expect(result).to include(:summary, :pools)
Expand All @@ -55,8 +55,8 @@ def miner_at(port)
# The POOLS_WITH_CONTROL_BYTE fixture contains a 0x01 byte.
# Without the gem's \uXXXX escape path, JSON.parse would reject
# it as invalid JSON.
responses = { 'pools' => CgminerFixtures::POOLS_WITH_CONTROL_BYTE }
FakeCgminer.with(responses: responses) do |port|
responses = { 'pools' => CgminerTestSupport::Fixtures::POOLS_WITH_CONTROL_BYTE }
CgminerTestSupport::FakeCgminer.with(responses: responses) do |port|
result = miner_at(port).pools
expect(result).to be_an(Array)
# The 0x01 byte is encoded as U+0001 in the parsed JSON
Expand All @@ -69,8 +69,8 @@ def miner_at(port)
describe 'parameter escaping on the wire' do
it 'doubles backslashes and escapes commas in outgoing parameters' do
received = []
FakeCgminer.with(
responses: { 'foo' => CgminerFixtures::SUMMARY },
CgminerTestSupport::FakeCgminer.with(
responses: { 'foo' => CgminerTestSupport::Fixtures::SUMMARY },
on_request: ->(bytes) { received << bytes }
) do |port|
miner_at(port).query(:foo, "a\\b,c")
Expand All @@ -87,17 +87,17 @@ def miner_at(port)

describe 'error handling' do
it 'raises ApiError when the server returns STATUS=E for an unknown command' do
FakeCgminer.with(responses: {}) do |port|
CgminerTestSupport::FakeCgminer.with(responses: {}) do |port|
expect { miner_at(port).query(:totally_fake_command) }
.to raise_error(CgminerApiClient::ApiError, /14: Invalid command/)
end
end

it 'returns false from #privileged when the server responds with access denied' do
responses = CgminerFixtures::DEFAULT.merge(
'privileged' => CgminerFixtures::PRIVILEGED_DENIED
responses = CgminerTestSupport::Fixtures::DEFAULT.merge(
'privileged' => CgminerTestSupport::Fixtures::PRIVILEGED_DENIED
)
FakeCgminer.with(responses: responses) do |port|
CgminerTestSupport::FakeCgminer.with(responses: responses) do |port|
expect(miner_at(port).privileged).to be(false)
end
end
Expand All @@ -121,16 +121,16 @@ def miner_at(port)
# socket instead of the real one, every admin verb that ships a secret
# (addpool password, setconfig/ascset/pgaset values) would break in
# production while the unit specs for on_wire redaction stay green.
# Assert against FakeCgminer's on_request hook, which captures the raw
# Assert against CgminerTestSupport::FakeCgminer's on_request hook, which captures the raw
# bytes the server received.
it 'sends the unredacted password to cgminer even when on_wire redacts it' do
received = []
responses = {
'addpool' => CgminerFixtures::ADDPOOL_OK,
'privileged' => CgminerFixtures::PRIVILEGED_OK
'addpool' => CgminerTestSupport::Fixtures::ADDPOOL_OK,
'privileged' => CgminerTestSupport::Fixtures::PRIVILEGED_OK
}

FakeCgminer.with(responses: responses, on_request: ->(bytes) { received << bytes }) do |port|
CgminerTestSupport::FakeCgminer.with(responses: responses, on_request: ->(bytes) { received << bytes }) do |port|
logged = []
miner = CgminerApiClient::Miner.new('127.0.0.1', port, 2,
on_wire: ->(*args) { logged << args })
Expand Down
6 changes: 1 addition & 5 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@

require 'cgminer_api_client'

# Load every support file (fixtures, FakeCgminer, etc). Fixtures
# must come before FakeCgminer since the latter references the
# former as a default — alphabetical order handles this since
# Dir[] returns sorted results on Ruby 3.0+.
Dir[File.expand_path('support/**/*.rb', __dir__)].each { |f| require f }
require 'cgminer_test_support'

RSpec.configure do |config|
config.filter_run :focus
Expand Down
101 changes: 0 additions & 101 deletions spec/support/cgminer_fixtures.rb

This file was deleted.

Loading
Loading