capydash 0.2.0 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +70 -171
- data/capydash.gemspec +7 -13
- data/lib/capydash/rspec.rb +567 -0
- data/lib/capydash/templates/report.html.erb +7 -24
- data/lib/capydash/version.rb +2 -2
- data/lib/capydash.rb +10 -59
- metadata +13 -84
- data/lib/capydash/auth.rb +0 -103
- data/lib/capydash/configuration.rb +0 -186
- data/lib/capydash/dashboard_server.rb +0 -167
- data/lib/capydash/engine.rb +0 -52
- data/lib/capydash/error_handler.rb +0 -101
- data/lib/capydash/event_emitter.rb +0 -29
- data/lib/capydash/forwarder.rb +0 -78
- data/lib/capydash/instrumentation.rb +0 -153
- data/lib/capydash/logger.rb +0 -99
- data/lib/capydash/persistence.rb +0 -134
- data/lib/capydash/report_generator.rb +0 -1007
- data/lib/capydash/rspec_integration.rb +0 -285
- data/lib/capydash/test_data_aggregator.rb +0 -221
- data/lib/capydash/test_data_collector.rb +0 -58
- data/lib/generators/capydash/install_generator.rb +0 -124
- data/lib/tasks/capydash.rake +0 -67
data/lib/capydash/engine.rb
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
module CapyDash
|
|
2
|
-
class Engine < ::Rails::Engine
|
|
3
|
-
isolate_namespace CapyDash
|
|
4
|
-
|
|
5
|
-
rake_tasks do
|
|
6
|
-
load File.expand_path("../tasks/capydash.rake", __dir__)
|
|
7
|
-
end
|
|
8
|
-
|
|
9
|
-
initializer "capydash.configuration", before: :load_config_initializers do
|
|
10
|
-
# Load configuration
|
|
11
|
-
CapyDash.config = CapyDash::Configuration.load_from_file
|
|
12
|
-
CapyDash::Logger.setup(CapyDash.config)
|
|
13
|
-
|
|
14
|
-
CapyDash::Logger.info("CapyDash configuration loaded", {
|
|
15
|
-
server_port: CapyDash.config.server_port,
|
|
16
|
-
log_level: CapyDash.config.log_level,
|
|
17
|
-
auth_enabled: CapyDash.config.auth_enabled?
|
|
18
|
-
})
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
initializer "capydash.instrumentation", after: :load_config_initializers do
|
|
22
|
-
begin
|
|
23
|
-
# Hook into Capybara sessions automatically
|
|
24
|
-
ActiveSupport.on_load(:action_dispatch_integration_test) do
|
|
25
|
-
Capybara::Session.include(CapyDash::Instrumentation)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Start WebSocket server automatically in dev/test unless using external WS
|
|
29
|
-
if (Rails.env.development? || Rails.env.test?) && ENV["CAPYDASH_EXTERNAL_WS"] != "1"
|
|
30
|
-
port = CapyDash.config.server_port
|
|
31
|
-
CapyDash::DashboardServer.instance(port: port).start
|
|
32
|
-
|
|
33
|
-
CapyDash::Logger.info("WebSocket server started", {
|
|
34
|
-
port: port,
|
|
35
|
-
environment: Rails.env
|
|
36
|
-
})
|
|
37
|
-
else
|
|
38
|
-
CapyDash::Logger.info("Skipping in-process WebSocket server", {
|
|
39
|
-
reason: ENV["CAPYDASH_EXTERNAL_WS"] == "1" ? "external_mode" : "production_environment",
|
|
40
|
-
environment: Rails.env
|
|
41
|
-
})
|
|
42
|
-
end
|
|
43
|
-
rescue => e
|
|
44
|
-
CapyDash::ErrorHandler.handle_error(e, {
|
|
45
|
-
error_type: 'initialization',
|
|
46
|
-
component: 'engine'
|
|
47
|
-
})
|
|
48
|
-
raise
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
module CapyDash
|
|
2
|
-
class ErrorHandler
|
|
3
|
-
class << self
|
|
4
|
-
def handle_error(error, context = {})
|
|
5
|
-
log_error(error, context)
|
|
6
|
-
notify_error(error, context) if should_notify?(error)
|
|
7
|
-
recover_from_error(error, context)
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def handle_websocket_error(error, connection = nil)
|
|
11
|
-
context = {
|
|
12
|
-
error_type: 'websocket',
|
|
13
|
-
connection_id: connection&.id,
|
|
14
|
-
error_class: error.class.name
|
|
15
|
-
}
|
|
16
|
-
handle_error(error, context)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def handle_test_execution_error(error, test_path = nil)
|
|
20
|
-
context = {
|
|
21
|
-
error_type: 'test_execution',
|
|
22
|
-
test_path: test_path,
|
|
23
|
-
error_class: error.class.name
|
|
24
|
-
}
|
|
25
|
-
handle_error(error, context)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def handle_instrumentation_error(error, method_name = nil)
|
|
29
|
-
context = {
|
|
30
|
-
error_type: 'instrumentation',
|
|
31
|
-
method_name: method_name,
|
|
32
|
-
error_class: error.class.name
|
|
33
|
-
}
|
|
34
|
-
handle_error(error, context)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
private
|
|
38
|
-
|
|
39
|
-
def log_error(error, context)
|
|
40
|
-
Logger.error(
|
|
41
|
-
"Error occurred: #{error.message}",
|
|
42
|
-
context.merge(
|
|
43
|
-
backtrace: error.backtrace&.first(5),
|
|
44
|
-
error_class: error.class.name
|
|
45
|
-
)
|
|
46
|
-
)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def notify_error(error, context)
|
|
50
|
-
# In a production app, this would send notifications to monitoring services
|
|
51
|
-
# like Sentry, Bugsnag, or custom webhooks
|
|
52
|
-
puts "🚨 CRITICAL ERROR: #{error.message}"
|
|
53
|
-
puts "Context: #{context}"
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def should_notify?(error)
|
|
57
|
-
# Only notify for critical errors
|
|
58
|
-
error.is_a?(StandardError) &&
|
|
59
|
-
!error.is_a?(ArgumentError) &&
|
|
60
|
-
!error.is_a?(NoMethodError)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def recover_from_error(error, context)
|
|
64
|
-
case context[:error_type]
|
|
65
|
-
when 'websocket'
|
|
66
|
-
recover_websocket_connection
|
|
67
|
-
when 'test_execution'
|
|
68
|
-
recover_test_execution
|
|
69
|
-
when 'instrumentation'
|
|
70
|
-
recover_instrumentation
|
|
71
|
-
else
|
|
72
|
-
# Generic recovery
|
|
73
|
-
Logger.warn("No specific recovery strategy for error type: #{context[:error_type]}")
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def recover_websocket_connection
|
|
78
|
-
Logger.info("Attempting to recover WebSocket connection")
|
|
79
|
-
# In a real implementation, this would attempt to reconnect
|
|
80
|
-
# or notify the dashboard to refresh the connection
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def recover_test_execution
|
|
84
|
-
Logger.info("Test execution error recovered")
|
|
85
|
-
# Could implement retry logic or cleanup
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def recover_instrumentation
|
|
89
|
-
Logger.info("Instrumentation error recovered")
|
|
90
|
-
# Could disable problematic instrumentation or use fallbacks
|
|
91
|
-
end
|
|
92
|
-
end
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
# Custom error classes for better error handling
|
|
96
|
-
class CapyDashError < StandardError; end
|
|
97
|
-
class ConfigurationError < CapyDashError; end
|
|
98
|
-
class WebSocketError < CapyDashError; end
|
|
99
|
-
class TestExecutionError < CapyDashError; end
|
|
100
|
-
class InstrumentationError < CapyDashError; end
|
|
101
|
-
end
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
require 'capydash/dashboard_server'
|
|
2
|
-
require 'capydash/forwarder'
|
|
3
|
-
|
|
4
|
-
module CapyDash
|
|
5
|
-
module EventEmitter
|
|
6
|
-
@clients = []
|
|
7
|
-
|
|
8
|
-
class << self
|
|
9
|
-
attr_accessor :clients
|
|
10
|
-
|
|
11
|
-
def subscribe(&block)
|
|
12
|
-
@on_event = block
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def broadcast(event)
|
|
16
|
-
@on_event&.call(event)
|
|
17
|
-
|
|
18
|
-
port = CapyDash.configuration&.port
|
|
19
|
-
if ENV["CAPYDASH_EXTERNAL_WS"] == "1"
|
|
20
|
-
forwarder = CapyDash::Forwarder.instance(port: port)
|
|
21
|
-
forwarder.send_message(event)
|
|
22
|
-
else
|
|
23
|
-
server = CapyDash::DashboardServer.instance(port: port)
|
|
24
|
-
server.broadcast(event.to_json)
|
|
25
|
-
end
|
|
26
|
-
end
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
data/lib/capydash/forwarder.rb
DELETED
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
require 'eventmachine'
|
|
2
|
-
require 'faye/websocket'
|
|
3
|
-
require 'json'
|
|
4
|
-
|
|
5
|
-
module CapyDash
|
|
6
|
-
# Forwards events to an external WebSocket server when CAPYDASH_EXTERNAL_WS=1
|
|
7
|
-
class Forwarder
|
|
8
|
-
attr_reader :port
|
|
9
|
-
|
|
10
|
-
def self.instance(port: nil)
|
|
11
|
-
configured_port = port || (CapyDash.respond_to?(:configuration) ? CapyDash.configuration&.port : nil)
|
|
12
|
-
@instance ||= new(port: configured_port || 4000)
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def initialize(port: 4000)
|
|
16
|
-
@port = port
|
|
17
|
-
@connected = false
|
|
18
|
-
@queue = []
|
|
19
|
-
@ws = nil
|
|
20
|
-
@mutex = Mutex.new
|
|
21
|
-
start
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def start
|
|
25
|
-
return if @started
|
|
26
|
-
@started = true
|
|
27
|
-
|
|
28
|
-
Thread.new do
|
|
29
|
-
EM.run do
|
|
30
|
-
connect
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def connect
|
|
36
|
-
url = "ws://127.0.0.1:#{@port}"
|
|
37
|
-
@ws = Faye::WebSocket::Client.new(url)
|
|
38
|
-
|
|
39
|
-
@ws.on(:open) do |_event|
|
|
40
|
-
@connected = true
|
|
41
|
-
flush_queue
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
@ws.on(:close) do |_event|
|
|
45
|
-
@connected = false
|
|
46
|
-
# attempt reconnect after short delay
|
|
47
|
-
EM.add_timer(0.5) { connect }
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
@ws.on(:error) do |event|
|
|
51
|
-
# keep trying; errors are expected if server not yet up
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def send_message(raw_message)
|
|
56
|
-
message = raw_message.is_a?(String) ? raw_message : JSON.dump(raw_message)
|
|
57
|
-
@mutex.synchronize do
|
|
58
|
-
if @connected && @ws
|
|
59
|
-
@ws.send(message)
|
|
60
|
-
else
|
|
61
|
-
@queue << message
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
private
|
|
67
|
-
|
|
68
|
-
def flush_queue
|
|
69
|
-
return unless @connected && @ws
|
|
70
|
-
pending = nil
|
|
71
|
-
@mutex.synchronize do
|
|
72
|
-
pending = @queue.dup
|
|
73
|
-
@queue.clear
|
|
74
|
-
end
|
|
75
|
-
pending.each { |m| @ws.send(m) }
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
require 'capybara'
|
|
2
|
-
require 'capydash/dashboard_server'
|
|
3
|
-
require 'base64'
|
|
4
|
-
require 'fileutils'
|
|
5
|
-
|
|
6
|
-
module CapyDash
|
|
7
|
-
module Instrumentation
|
|
8
|
-
def visit(path)
|
|
9
|
-
emit_step("visit", path) { super(path) }
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def click_button(*args)
|
|
13
|
-
emit_step("click_button", args.first) { super(*args) }
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def fill_in(locator, with:)
|
|
17
|
-
emit_step("fill_in", "#{locator} => #{with}") { super(locator, with: with) }
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
private
|
|
21
|
-
|
|
22
|
-
def emit_step(step_name, detail)
|
|
23
|
-
# emit "running" event first (without screenshot)
|
|
24
|
-
CapyDash::EventEmitter.broadcast(
|
|
25
|
-
step_name: step_name,
|
|
26
|
-
detail: detail,
|
|
27
|
-
test_name: (defined?(CapyDash) ? CapyDash.current_test : nil),
|
|
28
|
-
status: "running"
|
|
29
|
-
)
|
|
30
|
-
|
|
31
|
-
# run the original step
|
|
32
|
-
yield
|
|
33
|
-
|
|
34
|
-
# take screenshot AFTER the action is completed
|
|
35
|
-
base_dir = if CapyDash.respond_to?(:configuration)
|
|
36
|
-
CapyDash.configuration&.screenshot_path || "tmp/capydash_screenshots"
|
|
37
|
-
else
|
|
38
|
-
"tmp/capydash_screenshots"
|
|
39
|
-
end
|
|
40
|
-
FileUtils.mkdir_p(base_dir) unless Dir.exist?(base_dir)
|
|
41
|
-
timestamp = Time.now.strftime('%Y%m%d-%H%M%S-%L')
|
|
42
|
-
safe_step = step_name.gsub(/\s+/, '_')
|
|
43
|
-
screenshot_path = File.join(base_dir, "#{safe_step}-#{timestamp}.png")
|
|
44
|
-
|
|
45
|
-
# Ensure we have an absolute path for Base64 encoding
|
|
46
|
-
absolute_screenshot_path = File.absolute_path(screenshot_path)
|
|
47
|
-
|
|
48
|
-
data_url = nil
|
|
49
|
-
if defined?(Capybara)
|
|
50
|
-
begin
|
|
51
|
-
current_driver = Capybara.current_driver
|
|
52
|
-
if current_driver != :rack_test
|
|
53
|
-
if respond_to?(:page)
|
|
54
|
-
page.save_screenshot(screenshot_path)
|
|
55
|
-
else
|
|
56
|
-
Capybara.current_session.save_screenshot(screenshot_path)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Try to find the actual screenshot file location
|
|
60
|
-
actual_screenshot_path = nil
|
|
61
|
-
if File.exist?(absolute_screenshot_path)
|
|
62
|
-
actual_screenshot_path = absolute_screenshot_path
|
|
63
|
-
else
|
|
64
|
-
# Check if it was saved in the capybara tmp directory
|
|
65
|
-
capybara_path = File.join(Dir.pwd, "tmp", "capybara", screenshot_path)
|
|
66
|
-
if File.exist?(capybara_path)
|
|
67
|
-
actual_screenshot_path = capybara_path
|
|
68
|
-
end
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
if actual_screenshot_path && File.exist?(actual_screenshot_path)
|
|
72
|
-
encoded = Base64.strict_encode64(File.binread(actual_screenshot_path))
|
|
73
|
-
data_url = "data:image/png;base64,#{encoded}"
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
rescue => e
|
|
77
|
-
# Silently handle screenshot capture failures
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# mark success with screenshot
|
|
82
|
-
CapyDash::EventEmitter.broadcast(
|
|
83
|
-
step_name: step_name,
|
|
84
|
-
detail: detail,
|
|
85
|
-
screenshot: actual_screenshot_path || screenshot_path,
|
|
86
|
-
data_url: data_url,
|
|
87
|
-
test_name: (defined?(CapyDash) ? CapyDash.current_test : nil),
|
|
88
|
-
status: "passed"
|
|
89
|
-
)
|
|
90
|
-
rescue => e
|
|
91
|
-
# take screenshot even on failure (after the action was attempted)
|
|
92
|
-
base_dir = if CapyDash.respond_to?(:configuration)
|
|
93
|
-
CapyDash.configuration&.screenshot_path || "tmp/capydash_screenshots"
|
|
94
|
-
else
|
|
95
|
-
"tmp/capydash_screenshots"
|
|
96
|
-
end
|
|
97
|
-
FileUtils.mkdir_p(base_dir) unless Dir.exist?(base_dir)
|
|
98
|
-
timestamp = Time.now.strftime('%Y%m%d-%H%M%S-%L')
|
|
99
|
-
safe_step = step_name.gsub(/\s+/, '_')
|
|
100
|
-
screenshot_path = File.join(base_dir, "#{safe_step}-#{timestamp}.png")
|
|
101
|
-
|
|
102
|
-
# Ensure we have an absolute path for Base64 encoding
|
|
103
|
-
absolute_screenshot_path = File.absolute_path(screenshot_path)
|
|
104
|
-
|
|
105
|
-
data_url = nil
|
|
106
|
-
if defined?(Capybara)
|
|
107
|
-
begin
|
|
108
|
-
current_driver = Capybara.current_driver
|
|
109
|
-
if current_driver != :rack_test
|
|
110
|
-
if respond_to?(:page)
|
|
111
|
-
page.save_screenshot(screenshot_path)
|
|
112
|
-
else
|
|
113
|
-
Capybara.current_session.save_screenshot(screenshot_path)
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
# Try to find the actual screenshot file location
|
|
117
|
-
actual_screenshot_path = nil
|
|
118
|
-
if File.exist?(absolute_screenshot_path)
|
|
119
|
-
actual_screenshot_path = absolute_screenshot_path
|
|
120
|
-
else
|
|
121
|
-
# Check if it was saved in the capybara tmp directory
|
|
122
|
-
capybara_path = File.join(Dir.pwd, "tmp", "capybara", screenshot_path)
|
|
123
|
-
if File.exist?(capybara_path)
|
|
124
|
-
actual_screenshot_path = capybara_path
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
if actual_screenshot_path && File.exist?(actual_screenshot_path)
|
|
129
|
-
encoded = Base64.strict_encode64(File.binread(actual_screenshot_path))
|
|
130
|
-
data_url = "data:image/png;base64,#{encoded}"
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
rescue => e
|
|
134
|
-
# Silently handle screenshot capture failures
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
CapyDash::EventEmitter.broadcast(
|
|
139
|
-
step_name: step_name,
|
|
140
|
-
detail: detail,
|
|
141
|
-
screenshot: actual_screenshot_path || screenshot_path,
|
|
142
|
-
data_url: data_url,
|
|
143
|
-
test_name: (defined?(CapyDash) ? CapyDash.current_test : nil),
|
|
144
|
-
status: "failed",
|
|
145
|
-
error: e.message
|
|
146
|
-
)
|
|
147
|
-
raise e
|
|
148
|
-
end
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
# Prepend into Capybara DSL so it wraps all calls
|
|
153
|
-
Capybara::Session.prepend(CapyDash::Instrumentation)
|
data/lib/capydash/logger.rb
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
require 'logger'
|
|
2
|
-
require 'fileutils'
|
|
3
|
-
|
|
4
|
-
module CapyDash
|
|
5
|
-
class Logger
|
|
6
|
-
class << self
|
|
7
|
-
attr_accessor :instance
|
|
8
|
-
end
|
|
9
|
-
|
|
10
|
-
def initialize(config = nil)
|
|
11
|
-
@config = config || Configuration.new
|
|
12
|
-
@logger = create_logger
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def self.setup(config = nil)
|
|
16
|
-
self.instance = new(config)
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def self.info(message, context = {})
|
|
20
|
-
instance&.info(message, context)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def self.warn(message, context = {})
|
|
24
|
-
instance&.warn(message, context)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def self.error(message, context = {})
|
|
28
|
-
instance&.error(message, context)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def self.debug(message, context = {})
|
|
32
|
-
instance&.debug(message, context)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def info(message, context = {})
|
|
36
|
-
log(:info, message, context)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
def warn(message, context = {})
|
|
40
|
-
log(:warn, message, context)
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def error(message, context = {})
|
|
44
|
-
log(:error, message, context)
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def debug(message, context = {})
|
|
48
|
-
log(:debug, message, context)
|
|
49
|
-
end
|
|
50
|
-
|
|
51
|
-
private
|
|
52
|
-
|
|
53
|
-
def create_logger
|
|
54
|
-
# Ensure log directory exists
|
|
55
|
-
log_dir = File.dirname(@config.log_file)
|
|
56
|
-
FileUtils.mkdir_p(log_dir) unless Dir.exist?(log_dir)
|
|
57
|
-
|
|
58
|
-
# Create logger with file rotation
|
|
59
|
-
logger = ::Logger.new(
|
|
60
|
-
@config.log_file,
|
|
61
|
-
@config.max_files,
|
|
62
|
-
@config.max_file_size
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
# Set log level
|
|
66
|
-
logger.level = case @config.log_level.downcase
|
|
67
|
-
when 'debug' then ::Logger::DEBUG
|
|
68
|
-
when 'info' then ::Logger::INFO
|
|
69
|
-
when 'warn' then ::Logger::WARN
|
|
70
|
-
when 'error' then ::Logger::ERROR
|
|
71
|
-
else ::Logger::INFO
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# Set format
|
|
75
|
-
logger.formatter = proc do |severity, datetime, progname, msg|
|
|
76
|
-
"#{datetime.strftime('%Y-%m-%d %H:%M:%S')} [#{severity}] #{progname}: #{msg}\n"
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
logger
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def log(level, message, context = {})
|
|
83
|
-
return unless @logger
|
|
84
|
-
|
|
85
|
-
# Add context information
|
|
86
|
-
full_message = message
|
|
87
|
-
if context.any?
|
|
88
|
-
context_str = context.map { |k, v| "#{k}=#{v}" }.join(' ')
|
|
89
|
-
full_message = "#{message} | #{context_str}"
|
|
90
|
-
end
|
|
91
|
-
|
|
92
|
-
@logger.send(level, full_message)
|
|
93
|
-
rescue => e
|
|
94
|
-
# Fallback to stdout if logging fails
|
|
95
|
-
puts "Logging error: #{e.message}"
|
|
96
|
-
puts "#{level.upcase}: #{message}"
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
end
|
data/lib/capydash/persistence.rb
DELETED
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
require 'json'
|
|
2
|
-
require 'fileutils'
|
|
3
|
-
|
|
4
|
-
module CapyDash
|
|
5
|
-
class Persistence
|
|
6
|
-
class << self
|
|
7
|
-
def save_test_run(test_run_data)
|
|
8
|
-
ensure_data_directory
|
|
9
|
-
file_path = test_run_file_path(test_run_data[:id] || generate_run_id)
|
|
10
|
-
|
|
11
|
-
begin
|
|
12
|
-
File.write(file_path, JSON.pretty_generate(test_run_data))
|
|
13
|
-
file_path
|
|
14
|
-
rescue => e
|
|
15
|
-
ErrorHandler.handle_error(e, {
|
|
16
|
-
error_type: 'persistence',
|
|
17
|
-
operation: 'save_test_run',
|
|
18
|
-
run_id: test_run_data[:id]
|
|
19
|
-
})
|
|
20
|
-
nil
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
def load_test_run(run_id)
|
|
25
|
-
file_path = test_run_file_path(run_id)
|
|
26
|
-
return nil unless File.exist?(file_path)
|
|
27
|
-
|
|
28
|
-
begin
|
|
29
|
-
JSON.parse(File.read(file_path), symbolize_names: true)
|
|
30
|
-
rescue => e
|
|
31
|
-
ErrorHandler.handle_error(e, {
|
|
32
|
-
error_type: 'persistence',
|
|
33
|
-
operation: 'load_test_run',
|
|
34
|
-
run_id: run_id
|
|
35
|
-
})
|
|
36
|
-
nil
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
def list_test_runs(limit = 50)
|
|
41
|
-
ensure_data_directory
|
|
42
|
-
data_dir = data_directory
|
|
43
|
-
|
|
44
|
-
begin
|
|
45
|
-
Dir.glob(File.join(data_dir, "test_run_*.json"))
|
|
46
|
-
.sort_by { |f| File.mtime(f) }
|
|
47
|
-
.reverse
|
|
48
|
-
.first(limit)
|
|
49
|
-
.map do |file_path|
|
|
50
|
-
run_id = File.basename(file_path, '.json').gsub('test_run_', '')
|
|
51
|
-
{
|
|
52
|
-
id: run_id,
|
|
53
|
-
file_path: file_path,
|
|
54
|
-
created_at: File.mtime(file_path),
|
|
55
|
-
size: File.size(file_path)
|
|
56
|
-
}
|
|
57
|
-
end
|
|
58
|
-
rescue => e
|
|
59
|
-
ErrorHandler.handle_error(e, {
|
|
60
|
-
error_type: 'persistence',
|
|
61
|
-
operation: 'list_test_runs'
|
|
62
|
-
})
|
|
63
|
-
[]
|
|
64
|
-
end
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def delete_test_run(run_id)
|
|
68
|
-
file_path = test_run_file_path(run_id)
|
|
69
|
-
return false unless File.exist?(file_path)
|
|
70
|
-
|
|
71
|
-
begin
|
|
72
|
-
File.delete(file_path)
|
|
73
|
-
Logger.info("Test run deleted", { run_id: run_id })
|
|
74
|
-
true
|
|
75
|
-
rescue => e
|
|
76
|
-
ErrorHandler.handle_error(e, {
|
|
77
|
-
error_type: 'persistence',
|
|
78
|
-
operation: 'delete_test_run',
|
|
79
|
-
run_id: run_id
|
|
80
|
-
})
|
|
81
|
-
false
|
|
82
|
-
end
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def cleanup_old_runs(days_to_keep = 30)
|
|
86
|
-
ensure_data_directory
|
|
87
|
-
cutoff_time = Time.now - (days_to_keep * 24 * 60 * 60)
|
|
88
|
-
deleted_count = 0
|
|
89
|
-
|
|
90
|
-
begin
|
|
91
|
-
Dir.glob(File.join(data_directory, "test_run_*.json")).each do |file_path|
|
|
92
|
-
if File.mtime(file_path) < cutoff_time
|
|
93
|
-
run_id = File.basename(file_path, '.json').gsub('test_run_', '')
|
|
94
|
-
if delete_test_run(run_id)
|
|
95
|
-
deleted_count += 1
|
|
96
|
-
end
|
|
97
|
-
end
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
Logger.info("Cleanup completed", {
|
|
101
|
-
deleted_runs: deleted_count,
|
|
102
|
-
days_kept: days_to_keep
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
deleted_count
|
|
106
|
-
rescue => e
|
|
107
|
-
ErrorHandler.handle_error(e, {
|
|
108
|
-
error_type: 'persistence',
|
|
109
|
-
operation: 'cleanup_old_runs'
|
|
110
|
-
})
|
|
111
|
-
0
|
|
112
|
-
end
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
private
|
|
116
|
-
|
|
117
|
-
def data_directory
|
|
118
|
-
@data_directory ||= File.join(Dir.pwd, "tmp", "capydash_data")
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
def ensure_data_directory
|
|
122
|
-
FileUtils.mkdir_p(data_directory) unless Dir.exist?(data_directory)
|
|
123
|
-
end
|
|
124
|
-
|
|
125
|
-
def test_run_file_path(run_id)
|
|
126
|
-
File.join(data_directory, "test_run_#{run_id}.json")
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
def generate_run_id
|
|
130
|
-
"#{Time.now.to_i}_#{SecureRandom.hex(4)}"
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
end
|