capydash 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +70 -171
- data/capydash.gemspec +7 -13
- data/lib/capydash/rspec.rb +415 -0
- data/lib/capydash/templates/report.html.erb +4 -26
- data/lib/capydash/version.rb +2 -2
- data/lib/capydash.rb +3 -60
- 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/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
|