- Add log package tests (AccessLog and ShareLog Good/Bad/Ugly triads) - Fix nil pointer panic in pool.NewStrategyFactory when config is nil - Add Worker Hashrate Good/Bad/Ugly test triad - Add ConfigWatcher Start Bad test (nonexistent path) - Add FailoverStrategy CurrentPools Bad/Ugly, EnabledPools Good/Bad/Ugly, and NewStrategyFactory Good/Bad/Ugly test triads - Improve doc comments on Stats, StatsSummary, Workers, WorkerRecord with AX-compliant usage examples Co-Authored-By: Virgil <virgil@lethean.io>
341 lines
9.7 KiB
Go
341 lines
9.7 KiB
Go
package log
|
|
|
|
import (
|
|
"net"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"dappco.re/go/proxy"
|
|
)
|
|
|
|
// TestAccessLog_OnLogin_Good verifies a CONNECT line is written with the expected columns.
|
|
//
|
|
// al := log.NewAccessLog("/tmp/test-access.log")
|
|
// al.OnLogin(proxy.Event{Miner: miner}) // writes "CONNECT 10.0.0.1 WALLET XMRig/6.21.0"
|
|
func TestAccessLog_OnLogin_Good(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "access.log")
|
|
al := NewAccessLog(path)
|
|
defer al.Close()
|
|
|
|
miner := newTestMiner(t)
|
|
al.OnLogin(proxy.Event{Miner: miner})
|
|
al.Close()
|
|
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("expected log file to exist: %v", err)
|
|
}
|
|
line := strings.TrimSpace(string(data))
|
|
if !strings.Contains(line, "CONNECT") {
|
|
t.Fatalf("expected CONNECT in log line, got %q", line)
|
|
}
|
|
}
|
|
|
|
// TestAccessLog_OnLogin_Bad verifies a nil miner event does not panic or write anything.
|
|
//
|
|
// al := log.NewAccessLog("/tmp/test-access.log")
|
|
// al.OnLogin(proxy.Event{Miner: nil}) // no-op
|
|
func TestAccessLog_OnLogin_Bad(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "access.log")
|
|
al := NewAccessLog(path)
|
|
defer al.Close()
|
|
|
|
al.OnLogin(proxy.Event{Miner: nil})
|
|
al.Close()
|
|
|
|
if _, err := os.Stat(path); err == nil {
|
|
data, _ := os.ReadFile(path)
|
|
if len(data) > 0 {
|
|
t.Fatalf("expected no output for nil miner, got %q", string(data))
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestAccessLog_OnLogin_Ugly verifies a nil AccessLog does not panic.
|
|
//
|
|
// var al *log.AccessLog
|
|
// al.OnLogin(proxy.Event{Miner: miner}) // no-op, no panic
|
|
func TestAccessLog_OnLogin_Ugly(t *testing.T) {
|
|
var al *AccessLog
|
|
miner := newTestMiner(t)
|
|
al.OnLogin(proxy.Event{Miner: miner})
|
|
}
|
|
|
|
// TestAccessLog_OnClose_Good verifies a CLOSE line includes rx and tx byte counts.
|
|
//
|
|
// al := log.NewAccessLog("/tmp/test-access.log")
|
|
// al.OnClose(proxy.Event{Miner: miner}) // writes "CLOSE <ip> <user> rx=0 tx=0"
|
|
func TestAccessLog_OnClose_Good(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "access.log")
|
|
al := NewAccessLog(path)
|
|
defer al.Close()
|
|
|
|
miner := newTestMiner(t)
|
|
al.OnClose(proxy.Event{Miner: miner})
|
|
al.Close()
|
|
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("expected log file to exist: %v", err)
|
|
}
|
|
line := strings.TrimSpace(string(data))
|
|
if !strings.Contains(line, "CLOSE") {
|
|
t.Fatalf("expected CLOSE in log line, got %q", line)
|
|
}
|
|
if !strings.Contains(line, "rx=") {
|
|
t.Fatalf("expected rx= in log line, got %q", line)
|
|
}
|
|
if !strings.Contains(line, "tx=") {
|
|
t.Fatalf("expected tx= in log line, got %q", line)
|
|
}
|
|
}
|
|
|
|
// TestAccessLog_OnClose_Bad verifies a nil miner close event produces no output.
|
|
//
|
|
// al := log.NewAccessLog("/tmp/test-access.log")
|
|
// al.OnClose(proxy.Event{Miner: nil}) // no-op
|
|
func TestAccessLog_OnClose_Bad(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "access.log")
|
|
al := NewAccessLog(path)
|
|
defer al.Close()
|
|
|
|
al.OnClose(proxy.Event{Miner: nil})
|
|
al.Close()
|
|
|
|
if _, err := os.Stat(path); err == nil {
|
|
data, _ := os.ReadFile(path)
|
|
if len(data) > 0 {
|
|
t.Fatalf("expected no output for nil miner, got %q", string(data))
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestAccessLog_OnClose_Ugly verifies close on an empty-path log is a no-op.
|
|
//
|
|
// al := log.NewAccessLog("")
|
|
// al.OnClose(proxy.Event{Miner: miner}) // no-op, empty path
|
|
func TestAccessLog_OnClose_Ugly(t *testing.T) {
|
|
al := NewAccessLog("")
|
|
defer al.Close()
|
|
|
|
miner := newTestMiner(t)
|
|
al.OnClose(proxy.Event{Miner: miner})
|
|
}
|
|
|
|
// TestShareLog_OnAccept_Good verifies an ACCEPT line is written with diff and latency.
|
|
//
|
|
// sl := log.NewShareLog("/tmp/test-shares.log")
|
|
// sl.OnAccept(proxy.Event{Miner: miner, Diff: 100000, Latency: 82})
|
|
func TestShareLog_OnAccept_Good(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "shares.log")
|
|
sl := NewShareLog(path)
|
|
defer sl.Close()
|
|
|
|
miner := newTestMiner(t)
|
|
sl.OnAccept(proxy.Event{Miner: miner, Diff: 100000, Latency: 82})
|
|
sl.Close()
|
|
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("expected log file to exist: %v", err)
|
|
}
|
|
line := strings.TrimSpace(string(data))
|
|
if !strings.Contains(line, "ACCEPT") {
|
|
t.Fatalf("expected ACCEPT in log line, got %q", line)
|
|
}
|
|
if !strings.Contains(line, "diff=100000") {
|
|
t.Fatalf("expected diff=100000 in log line, got %q", line)
|
|
}
|
|
if !strings.Contains(line, "latency=82ms") {
|
|
t.Fatalf("expected latency=82ms in log line, got %q", line)
|
|
}
|
|
}
|
|
|
|
// TestShareLog_OnAccept_Bad verifies a nil miner accept event produces no output.
|
|
//
|
|
// sl := log.NewShareLog("/tmp/test-shares.log")
|
|
// sl.OnAccept(proxy.Event{Miner: nil}) // no-op
|
|
func TestShareLog_OnAccept_Bad(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "shares.log")
|
|
sl := NewShareLog(path)
|
|
defer sl.Close()
|
|
|
|
sl.OnAccept(proxy.Event{Miner: nil, Diff: 100000})
|
|
sl.Close()
|
|
|
|
if _, err := os.Stat(path); err == nil {
|
|
data, _ := os.ReadFile(path)
|
|
if len(data) > 0 {
|
|
t.Fatalf("expected no output for nil miner, got %q", string(data))
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestShareLog_OnAccept_Ugly verifies a nil ShareLog does not panic.
|
|
//
|
|
// var sl *log.ShareLog
|
|
// sl.OnAccept(proxy.Event{Miner: miner}) // no-op, no panic
|
|
func TestShareLog_OnAccept_Ugly(t *testing.T) {
|
|
var sl *ShareLog
|
|
miner := newTestMiner(t)
|
|
sl.OnAccept(proxy.Event{Miner: miner, Diff: 100000})
|
|
}
|
|
|
|
// TestShareLog_OnReject_Good verifies a REJECT line is written with the rejection reason.
|
|
//
|
|
// sl := log.NewShareLog("/tmp/test-shares.log")
|
|
// sl.OnReject(proxy.Event{Miner: miner, Error: "Low difficulty share"})
|
|
func TestShareLog_OnReject_Good(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "shares.log")
|
|
sl := NewShareLog(path)
|
|
defer sl.Close()
|
|
|
|
miner := newTestMiner(t)
|
|
sl.OnReject(proxy.Event{Miner: miner, Error: "Low difficulty share"})
|
|
sl.Close()
|
|
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("expected log file to exist: %v", err)
|
|
}
|
|
line := strings.TrimSpace(string(data))
|
|
if !strings.Contains(line, "REJECT") {
|
|
t.Fatalf("expected REJECT in log line, got %q", line)
|
|
}
|
|
if !strings.Contains(line, "Low difficulty share") {
|
|
t.Fatalf("expected rejection reason in log line, got %q", line)
|
|
}
|
|
}
|
|
|
|
// TestShareLog_OnReject_Bad verifies a nil miner reject event produces no output.
|
|
//
|
|
// sl := log.NewShareLog("/tmp/test-shares.log")
|
|
// sl.OnReject(proxy.Event{Miner: nil}) // no-op
|
|
func TestShareLog_OnReject_Bad(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "shares.log")
|
|
sl := NewShareLog(path)
|
|
defer sl.Close()
|
|
|
|
sl.OnReject(proxy.Event{Miner: nil, Error: "Low difficulty share"})
|
|
sl.Close()
|
|
|
|
if _, err := os.Stat(path); err == nil {
|
|
data, _ := os.ReadFile(path)
|
|
if len(data) > 0 {
|
|
t.Fatalf("expected no output for nil miner, got %q", string(data))
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestShareLog_OnReject_Ugly verifies an empty-path ShareLog silently discards the reject line.
|
|
//
|
|
// sl := log.NewShareLog("")
|
|
// sl.OnReject(proxy.Event{Miner: miner, Error: "reason"}) // no-op, empty path
|
|
func TestShareLog_OnReject_Ugly(t *testing.T) {
|
|
sl := NewShareLog("")
|
|
defer sl.Close()
|
|
|
|
miner := newTestMiner(t)
|
|
sl.OnReject(proxy.Event{Miner: miner, Error: "reason"})
|
|
}
|
|
|
|
// TestAccessLog_Close_Good verifies Close releases the file handle and is safe to call twice.
|
|
//
|
|
// al := log.NewAccessLog("/tmp/test-access.log")
|
|
// al.OnLogin(proxy.Event{Miner: miner})
|
|
// al.Close()
|
|
// al.Close() // double close is safe
|
|
func TestAccessLog_Close_Good(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "access.log")
|
|
al := NewAccessLog(path)
|
|
|
|
miner := newTestMiner(t)
|
|
al.OnLogin(proxy.Event{Miner: miner})
|
|
al.Close()
|
|
al.Close()
|
|
}
|
|
|
|
// TestAccessLog_Close_Bad verifies Close on a nil AccessLog does not panic.
|
|
//
|
|
// var al *log.AccessLog
|
|
// al.Close() // no-op, no panic
|
|
func TestAccessLog_Close_Bad(t *testing.T) {
|
|
var al *AccessLog
|
|
al.Close()
|
|
}
|
|
|
|
// TestAccessLog_Close_Ugly verifies Close on a never-opened log does not panic.
|
|
//
|
|
// al := log.NewAccessLog("/nonexistent/dir/access.log")
|
|
// al.Close() // no file was ever opened
|
|
func TestAccessLog_Close_Ugly(t *testing.T) {
|
|
al := NewAccessLog("/nonexistent/dir/access.log")
|
|
al.Close()
|
|
}
|
|
|
|
// TestShareLog_Close_Good verifies Close releases the file handle and is safe to call twice.
|
|
//
|
|
// sl := log.NewShareLog("/tmp/test-shares.log")
|
|
// sl.OnAccept(proxy.Event{Miner: miner, Diff: 1000})
|
|
// sl.Close()
|
|
// sl.Close() // double close is safe
|
|
func TestShareLog_Close_Good(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "shares.log")
|
|
sl := NewShareLog(path)
|
|
|
|
miner := newTestMiner(t)
|
|
sl.OnAccept(proxy.Event{Miner: miner, Diff: 1000})
|
|
sl.Close()
|
|
sl.Close()
|
|
}
|
|
|
|
// TestShareLog_Close_Bad verifies Close on a nil ShareLog does not panic.
|
|
//
|
|
// var sl *log.ShareLog
|
|
// sl.Close() // no-op, no panic
|
|
func TestShareLog_Close_Bad(t *testing.T) {
|
|
var sl *ShareLog
|
|
sl.Close()
|
|
}
|
|
|
|
// TestShareLog_Close_Ugly verifies Close on a never-opened log does not panic.
|
|
//
|
|
// sl := log.NewShareLog("/nonexistent/dir/shares.log")
|
|
// sl.Close() // no file was ever opened
|
|
func TestShareLog_Close_Ugly(t *testing.T) {
|
|
sl := NewShareLog("/nonexistent/dir/shares.log")
|
|
sl.Close()
|
|
}
|
|
|
|
// newTestMiner creates a minimal miner for log testing using a net.Pipe connection.
|
|
func newTestMiner(t *testing.T) *proxy.Miner {
|
|
t.Helper()
|
|
client, server := net.Pipe()
|
|
t.Cleanup(func() {
|
|
_ = client.Close()
|
|
_ = server.Close()
|
|
})
|
|
miner := proxy.NewMiner(client, 3333, nil)
|
|
miner.SetID(1)
|
|
return miner
|
|
}
|
|
|
|
// pipeAddr satisfies the net.Addr interface for pipe-based test connections.
|
|
type pipeAddr struct{}
|
|
|
|
func (a pipeAddr) Network() string { return "pipe" }
|
|
func (a pipeAddr) String() string { return "pipe" }
|
|
|
|
// pipeConn wraps an os.Pipe as a net.Conn for tests that need a closeable socket.
|
|
type pipeConn struct {
|
|
*os.File
|
|
}
|
|
|
|
func (p *pipeConn) RemoteAddr() net.Addr { return pipeAddr{} }
|
|
func (p *pipeConn) LocalAddr() net.Addr { return pipeAddr{} }
|
|
func (p *pipeConn) SetDeadline(_ time.Time) error { return nil }
|
|
func (p *pipeConn) SetReadDeadline(_ time.Time) error { return nil }
|
|
func (p *pipeConn) SetWriteDeadline(_ time.Time) error { return nil }
|