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.
data/lib/capydash.rb CHANGED
@@ -1,64 +1,15 @@
1
- require 'ostruct'
2
1
  require "capydash/version"
3
- require "capydash/engine"
4
- require "capydash/instrumentation"
5
- require "capydash/event_emitter"
6
- require "capydash/dashboard_server"
7
- require "capydash/configuration"
8
- require "capydash/logger"
9
- require "capydash/error_handler"
10
- require "capydash/persistence"
11
- require "capydash/auth"
12
- require "capydash/test_data_collector"
13
- require "capydash/test_data_aggregator"
14
- require "capydash/report_generator"
15
2
 
16
- # Conditionally load RSpec integration if RSpec is present
17
- if defined?(RSpec)
18
- require "capydash/rspec_integration"
19
- CapyDash::RSpecIntegration.setup!
20
- end
21
-
22
- module CapyDash
23
- class << self
24
- attr_accessor :configuration, :current_test, :config
25
-
26
- def configure
27
- self.configuration ||= OpenStruct.new
28
- yield(configuration)
29
- end
30
-
31
- def config
32
- @config ||= Configuration.load_from_file
33
- end
34
-
35
- def config=(new_config)
36
- @config = new_config
37
- end
38
-
39
- # Convenience methods for common operations
40
- def log_info(message, context = {})
41
- Logger.info(message, context)
42
- end
43
-
44
- def log_error(message, context = {})
45
- Logger.error(message, context)
46
- end
47
-
48
- def handle_error(error, context = {})
49
- ErrorHandler.handle_error(error, context)
50
- end
51
-
52
- def save_test_run(data)
53
- Persistence.save_test_run(data)
54
- end
55
-
56
- def load_test_run(run_id)
57
- Persistence.load_test_run(run_id)
58
- end
59
-
60
- def list_test_runs(limit = 50)
61
- Persistence.list_test_runs(limit)
3
+ # Auto-setup RSpec integration if RSpec is present and ready
4
+ if defined?(RSpec) && RSpec.respond_to?(:configure)
5
+ require "capydash/rspec"
6
+ CapyDash::RSpec.setup!
7
+ elsif defined?(Rails)
8
+ # In Rails, RSpec might load after the gem, so set up a hook
9
+ Rails.application.config.after_initialize do
10
+ if defined?(RSpec) && RSpec.respond_to?(:configure)
11
+ require "capydash/rspec" unless defined?(CapyDash::RSpec)
12
+ CapyDash::RSpec.setup!
62
13
  end
63
14
  end
64
15
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: capydash
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Damon Clark
@@ -10,21 +10,7 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
- name: railties
14
- requirement: !ruby/object:Gem::Requirement
15
- requirements:
16
- - - ">="
17
- - !ruby/object:Gem::Version
18
- version: '5.0'
19
- type: :runtime
20
- prerelease: false
21
- version_requirements: !ruby/object:Gem::Requirement
22
- requirements:
23
- - - ">="
24
- - !ruby/object:Gem::Version
25
- version: '5.0'
26
- - !ruby/object:Gem::Dependency
27
- name: capybara
13
+ name: rspec
28
14
  requirement: !ruby/object:Gem::Requirement
29
15
  requirements:
30
16
  - - ">="
@@ -38,77 +24,35 @@ dependencies:
38
24
  - !ruby/object:Gem::Version
39
25
  version: '3.0'
40
26
  - !ruby/object:Gem::Dependency
41
- name: faye-websocket
42
- requirement: !ruby/object:Gem::Requirement
43
- requirements:
44
- - - ">="
45
- - !ruby/object:Gem::Version
46
- version: '0'
47
- type: :runtime
48
- prerelease: false
49
- version_requirements: !ruby/object:Gem::Requirement
50
- requirements:
51
- - - ">="
52
- - !ruby/object:Gem::Version
53
- version: '0'
54
- - !ruby/object:Gem::Dependency
55
- name: eventmachine
56
- requirement: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - ">="
59
- - !ruby/object:Gem::Version
60
- version: '0'
61
- type: :runtime
62
- prerelease: false
63
- version_requirements: !ruby/object:Gem::Requirement
64
- requirements:
65
- - - ">="
66
- - !ruby/object:Gem::Version
67
- version: '0'
68
- - !ruby/object:Gem::Dependency
69
- name: em-websocket
70
- requirement: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - ">="
73
- - !ruby/object:Gem::Version
74
- version: '0'
75
- type: :runtime
76
- prerelease: false
77
- version_requirements: !ruby/object:Gem::Requirement
78
- requirements:
79
- - - ">="
80
- - !ruby/object:Gem::Version
81
- version: '0'
82
- - !ruby/object:Gem::Dependency
83
- name: rspec
27
+ name: rspec-rails
84
28
  requirement: !ruby/object:Gem::Requirement
85
29
  requirements:
86
30
  - - "~>"
87
31
  - !ruby/object:Gem::Version
88
- version: '3.0'
32
+ version: '6.0'
89
33
  type: :development
90
34
  prerelease: false
91
35
  version_requirements: !ruby/object:Gem::Requirement
92
36
  requirements:
93
37
  - - "~>"
94
38
  - !ruby/object:Gem::Version
95
- version: '3.0'
39
+ version: '6.0'
96
40
  - !ruby/object:Gem::Dependency
97
41
  name: rails
98
42
  requirement: !ruby/object:Gem::Requirement
99
43
  requirements:
100
- - - "~>"
44
+ - - ">="
101
45
  - !ruby/object:Gem::Version
102
- version: '8.0'
46
+ version: '6.0'
103
47
  type: :development
104
48
  prerelease: false
105
49
  version_requirements: !ruby/object:Gem::Requirement
106
50
  requirements:
107
- - - "~>"
51
+ - - ">="
108
52
  - !ruby/object:Gem::Version
109
- version: '8.0'
110
- description: CapyDash instruments Capybara tests and streams test steps, screenshots,
111
- and DOM snapshots to a live dashboard.
53
+ version: '6.0'
54
+ description: CapyDash automatically generates clean, readable HTML test reports after
55
+ your RSpec suite finishes. Zero configuration required.
112
56
  email:
113
57
  - dclark312@gmail.com
114
58
  executables: []
@@ -118,24 +62,9 @@ files:
118
62
  - README.md
119
63
  - capydash.gemspec
120
64
  - lib/capydash.rb
121
- - lib/capydash/auth.rb
122
- - lib/capydash/configuration.rb
123
- - lib/capydash/dashboard_server.rb
124
- - lib/capydash/engine.rb
125
- - lib/capydash/error_handler.rb
126
- - lib/capydash/event_emitter.rb
127
- - lib/capydash/forwarder.rb
128
- - lib/capydash/instrumentation.rb
129
- - lib/capydash/logger.rb
130
- - lib/capydash/persistence.rb
131
- - lib/capydash/report_generator.rb
132
- - lib/capydash/rspec_integration.rb
65
+ - lib/capydash/rspec.rb
133
66
  - lib/capydash/templates/report.html.erb
134
- - lib/capydash/test_data_aggregator.rb
135
- - lib/capydash/test_data_collector.rb
136
67
  - lib/capydash/version.rb
137
- - lib/generators/capydash/install_generator.rb
138
- - lib/tasks/capydash.rake
139
68
  homepage: https://github.com/damonclark/capydash
140
69
  licenses:
141
70
  - MIT
@@ -157,5 +86,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
157
86
  requirements: []
158
87
  rubygems_version: 3.6.7
159
88
  specification_version: 4
160
- summary: Real-time Capybara test dashboard
89
+ summary: Minimal static HTML report generator for RSpec system tests
161
90
  test_files: []
data/lib/capydash/auth.rb DELETED
@@ -1,103 +0,0 @@
1
- require 'securerandom'
2
- require 'digest'
3
-
4
- module CapyDash
5
- class Auth
6
- class << self
7
- def authenticate(username, password)
8
- return false unless auth_enabled?
9
-
10
- # Simple hardcoded credentials for MVP
11
- # In production, this would connect to a proper user database
12
- valid_credentials = {
13
- 'admin' => 'capydash123',
14
- 'developer' => 'test123',
15
- 'viewer' => 'readonly123'
16
- }
17
-
18
- if valid_credentials[username] == password
19
- token = generate_token(username)
20
- Logger.info("User authenticated", {
21
- username: username,
22
- token: token[0..8] + "..."
23
- })
24
- token
25
- else
26
- Logger.warn("Authentication failed", {
27
- username: username,
28
- ip: current_ip
29
- })
30
- nil
31
- end
32
- end
33
-
34
- def validate_token(token)
35
- return false unless auth_enabled?
36
- return false unless token && token.length > 10
37
-
38
- # Simple token validation for MVP
39
- # In production, this would use JWT or similar
40
- begin
41
- decoded = Base64.decode64(token)
42
- parts = decoded.split(':')
43
- return false unless parts.length == 3
44
-
45
- username, timestamp, signature = parts
46
- expected_signature = generate_signature(username, timestamp)
47
-
48
- if signature == expected_signature
49
- # Check if token is not expired (24 hours)
50
- token_time = Time.at(timestamp.to_i)
51
- if Time.now - token_time < 24 * 60 * 60
52
- Logger.debug("Token validated", { username: username })
53
- username
54
- else
55
- Logger.warn("Token expired", { username: username })
56
- false
57
- end
58
- else
59
- Logger.warn("Invalid token signature", { token: token[0..8] + "..." })
60
- false
61
- end
62
- rescue => e
63
- ErrorHandler.handle_error(e, {
64
- error_type: 'authentication',
65
- operation: 'validate_token'
66
- })
67
- false
68
- end
69
- end
70
-
71
- def auth_enabled?
72
- CapyDash.config.auth_enabled?
73
- end
74
-
75
- def require_auth!
76
- return true unless auth_enabled?
77
- # This would be called by middleware or controllers
78
- # For now, just return true
79
- true
80
- end
81
-
82
- private
83
-
84
- def generate_token(username)
85
- timestamp = Time.now.to_i.to_s
86
- signature = generate_signature(username, timestamp)
87
- token_data = "#{username}:#{timestamp}:#{signature}"
88
- Base64.encode64(token_data).strip
89
- end
90
-
91
- def generate_signature(username, timestamp)
92
- secret = CapyDash.config.secret_key
93
- data = "#{username}:#{timestamp}:#{secret}"
94
- Digest::SHA256.hexdigest(data)
95
- end
96
-
97
- def current_ip
98
- # In a real app, this would get the actual IP
99
- "127.0.0.1"
100
- end
101
- end
102
- end
103
- end
@@ -1,186 +0,0 @@
1
- module CapyDash
2
- class Configuration
3
- attr_accessor :server, :dashboard, :tests, :logging, :security, :performance
4
-
5
- def initialize
6
- @server = {
7
- host: "localhost",
8
- port: 4000,
9
- websocket_path: "/websocket",
10
- max_connections: 100,
11
- message_history_limit: 1000
12
- }
13
-
14
- @dashboard = {
15
- title: "CapyDash Test Monitor",
16
- refresh_interval: 1000,
17
- auto_scroll: true,
18
- show_timestamps: true,
19
- screenshot_quality: 0.8,
20
- max_screenshot_width: 1200
21
- }
22
-
23
- @tests = {
24
- default_directory: "test",
25
- system_tests_dir: "test/system",
26
- feature_tests_dir: "test/features",
27
- controller_tests_dir: "test/controllers",
28
- model_tests_dir: "test/models",
29
- screenshot_dir: "tmp/capydash_screenshots",
30
- timeout: 300
31
- }
32
-
33
- @logging = {
34
- level: "info",
35
- file: "log/capydash.log",
36
- max_file_size: "10MB",
37
- max_files: 5
38
- }
39
-
40
- @security = {
41
- enable_auth: false,
42
- secret_key: "your-secret-key-here",
43
- session_timeout: 3600
44
- }
45
-
46
- @performance = {
47
- enable_compression: true,
48
- cleanup_interval: 300,
49
- max_memory_usage: "512MB"
50
- }
51
- end
52
-
53
- def self.load_from_file(config_path = nil)
54
- config_path ||= File.join(Dir.pwd, "config", "capydash.yml")
55
-
56
- if File.exist?(config_path)
57
- require 'yaml'
58
- yaml_config = YAML.load_file(config_path)
59
-
60
- config = new
61
- config.load_from_hash(yaml_config)
62
- config
63
- else
64
- # Return default configuration if file doesn't exist
65
- new
66
- end
67
- rescue => e
68
- puts "Warning: Could not load configuration from #{config_path}: #{e.message}"
69
- puts "Using default configuration."
70
- new
71
- end
72
-
73
- def load_from_hash(hash)
74
- @server.merge!(hash['server']) if hash['server']
75
- @dashboard.merge!(hash['dashboard']) if hash['dashboard']
76
- @tests.merge!(hash['tests']) if hash['tests']
77
- @logging.merge!(hash['logging']) if hash['logging']
78
- @security.merge!(hash['security']) if hash['security']
79
- @performance.merge!(hash['performance']) if hash['performance']
80
- end
81
-
82
- def server_host
83
- @server[:host]
84
- end
85
-
86
- def server_port
87
- @server[:port]
88
- end
89
-
90
- def websocket_path
91
- @server[:websocket_path]
92
- end
93
-
94
- def max_connections
95
- @server[:max_connections]
96
- end
97
-
98
- def message_history_limit
99
- @server[:message_history_limit]
100
- end
101
-
102
- def dashboard_title
103
- @dashboard[:title]
104
- end
105
-
106
- def auto_scroll?
107
- @dashboard[:auto_scroll]
108
- end
109
-
110
- def show_timestamps?
111
- @dashboard[:show_timestamps]
112
- end
113
-
114
- def screenshot_quality
115
- @dashboard[:screenshot_quality]
116
- end
117
-
118
- def max_screenshot_width
119
- @dashboard[:max_screenshot_width]
120
- end
121
-
122
- def system_tests_dir
123
- @tests[:system_tests_dir]
124
- end
125
-
126
- def feature_tests_dir
127
- @tests[:feature_tests_dir]
128
- end
129
-
130
- def controller_tests_dir
131
- @tests[:controller_tests_dir]
132
- end
133
-
134
- def model_tests_dir
135
- @tests[:model_tests_dir]
136
- end
137
-
138
- def screenshot_dir
139
- @tests[:screenshot_dir]
140
- end
141
-
142
- def test_timeout
143
- @tests[:timeout]
144
- end
145
-
146
- def log_level
147
- @logging[:level]
148
- end
149
-
150
- def log_file
151
- @logging[:file]
152
- end
153
-
154
- def max_files
155
- @logging[:max_files]
156
- end
157
-
158
- def max_file_size
159
- @logging[:max_file_size]
160
- end
161
-
162
- def auth_enabled?
163
- @security[:enable_auth]
164
- end
165
-
166
- def secret_key
167
- @security[:secret_key]
168
- end
169
-
170
- def session_timeout
171
- @security[:session_timeout]
172
- end
173
-
174
- def compression_enabled?
175
- @performance[:enable_compression]
176
- end
177
-
178
- def cleanup_interval
179
- @performance[:cleanup_interval]
180
- end
181
-
182
- def max_memory_usage
183
- @performance[:max_memory_usage]
184
- end
185
- end
186
- end
@@ -1,167 +0,0 @@
1
- require 'eventmachine'
2
- require 'em-websocket'
3
-
4
- module CapyDash
5
- class DashboardServer
6
- attr_reader :port, :clients
7
-
8
- # Provide a single shared instance for the whole process
9
- def self.instance(port: nil)
10
- configured_port = port || CapyDash.config.server_port
11
- @instance ||= new(port: configured_port)
12
- end
13
-
14
- def initialize(port: nil)
15
- @port = port || CapyDash.config.server_port
16
- @clients = []
17
- @history = [] # recent events to replay on reconnect
18
- @history_limit = CapyDash.config.message_history_limit
19
- @max_connections = CapyDash.config.max_connections
20
- end
21
-
22
- # Start the WebSocket server in a background thread
23
- def start
24
- return if defined?(@started) && @started
25
- @started = true
26
- Thread.new do
27
- EM.run do
28
- EM::WebSocket.run(host: "0.0.0.0", port: @port) do |ws|
29
-
30
- ws.onopen do
31
- begin
32
- # Check connection limit
33
- if @clients.length >= @max_connections
34
- CapyDash::Logger.warn("Connection limit reached", {
35
- current_connections: @clients.length,
36
- max_connections: @max_connections
37
- })
38
- ws.close
39
- return
40
- end
41
-
42
- @clients << ws
43
- CapyDash::Logger.info("Client connected", {
44
- total_clients: @clients.length,
45
- client_id: ws.object_id
46
- })
47
- puts "Client connected"
48
-
49
- # Replay recent history so refreshed clients see last events
50
- @history.each do |msg|
51
- begin
52
- ws.send(msg)
53
- rescue => e
54
- CapyDash::ErrorHandler.handle_websocket_error(e, ws)
55
- end
56
- end
57
- rescue => e
58
- CapyDash::ErrorHandler.handle_websocket_error(e, ws)
59
- end
60
- end
61
-
62
- ws.onclose do
63
- begin
64
- @clients.delete(ws)
65
- CapyDash::Logger.info("Client disconnected", {
66
- remaining_clients: @clients.length,
67
- client_id: ws.object_id
68
- })
69
- puts "Client disconnected"
70
- rescue => e
71
- CapyDash::ErrorHandler.handle_websocket_error(e, ws)
72
- end
73
- end
74
-
75
- ws.onmessage do |msg|
76
- begin
77
- CapyDash::Logger.debug("Received WebSocket message", {
78
- message_length: msg.length,
79
- client_id: ws.object_id
80
- })
81
- puts "Received message: #{msg}"
82
-
83
- data = JSON.parse(msg) rescue nil
84
- if data && data["command"] == "run_tests"
85
- args = data["args"] || ["bundle", "exec", "rails", "test", "test/system"]
86
- CapyDash::Logger.info("Executing test command", {
87
- command: args.join(' '),
88
- client_id: ws.object_id
89
- })
90
-
91
- Thread.new do
92
- begin
93
- # Change to the dummy app directory and run tests there
94
- current_dir = File.dirname(__FILE__)
95
- gem_root = File.expand_path(File.join(current_dir, "..", ".."))
96
- dummy_app_path = File.join(gem_root, "spec", "dummy_app")
97
-
98
- CapyDash::Logger.info("Running tests in directory", {
99
- directory: dummy_app_path,
100
- exists: Dir.exist?(dummy_app_path)
101
- })
102
-
103
- Dir.chdir(dummy_app_path) do
104
- # Set Rails environment and ensure CapyDash is loaded
105
- ENV["RAILS_ENV"] = "test"
106
- ENV["CAPYDASH_EXTERNAL_WS"] = "1" # Use external WebSocket mode
107
-
108
-
109
- # Run the command and capture both stdout and stderr
110
- IO.popen(args, err: [:child, :out]) do |io|
111
- io.each_line do |line|
112
- event = { type: "runner", line: line.strip, status: "running", ts: Time.now.to_i }
113
- broadcast(event.to_json)
114
- end
115
- end
116
- end
117
- rescue => e
118
- CapyDash::ErrorHandler.handle_test_execution_error(e, args.join(' '))
119
- broadcast({ type: "runner", line: "Error: #{e.message}", status: "failed", ts: Time.now.to_i }.to_json)
120
- end
121
- broadcast({ type: "runner", line: "Finished", status: "passed", ts: Time.now.to_i }.to_json)
122
- end
123
- else
124
- # Optionally broadcast to all clients
125
- broadcast(msg)
126
- end
127
- rescue => e
128
- CapyDash::ErrorHandler.handle_websocket_error(e, ws)
129
- end
130
- end
131
-
132
- end
133
- end
134
- end
135
- end
136
-
137
- # Send a message to all connected clients
138
- def broadcast(message)
139
- begin
140
- # store in history buffer
141
- @history << message
142
- @history.shift if @history.length > @history_limit
143
-
144
- # fan out to all connected clients
145
- @clients.each do |client|
146
- begin
147
- client.send(message)
148
- rescue => e
149
- CapyDash::ErrorHandler.handle_websocket_error(e, client)
150
- # Remove dead connections
151
- @clients.delete(client)
152
- end
153
- end
154
- rescue => e
155
- CapyDash::ErrorHandler.handle_error(e, {
156
- error_type: 'broadcast',
157
- message_length: message&.length
158
- })
159
- end
160
- end
161
-
162
- # Stop the WebSocket server gracefully
163
- def stop
164
- EM.stop_event_loop if EM.reactor_running?
165
- end
166
- end
167
- end