orfeas_lyra 0.6.0
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 +7 -0
- data/CHANGELOG.md +222 -0
- data/LICENSE +21 -0
- data/README.md +1165 -0
- data/Rakefile +728 -0
- data/app/controllers/lyra/application_controller.rb +23 -0
- data/app/controllers/lyra/dashboard_controller.rb +624 -0
- data/app/controllers/lyra/flow_controller.rb +224 -0
- data/app/controllers/lyra/privacy_controller.rb +182 -0
- data/app/views/lyra/dashboard/audit_trail.html.erb +324 -0
- data/app/views/lyra/dashboard/discrepancies.html.erb +125 -0
- data/app/views/lyra/dashboard/event_graph_view.html.erb +525 -0
- data/app/views/lyra/dashboard/heatmap_view.html.erb +155 -0
- data/app/views/lyra/dashboard/index.html.erb +119 -0
- data/app/views/lyra/dashboard/model_overview.html.erb +115 -0
- data/app/views/lyra/dashboard/projections.html.erb +302 -0
- data/app/views/lyra/dashboard/schema.html.erb +283 -0
- data/app/views/lyra/dashboard/schema_history.html.erb +78 -0
- data/app/views/lyra/dashboard/schema_version.html.erb +340 -0
- data/app/views/lyra/dashboard/verification.html.erb +370 -0
- data/app/views/lyra/flow/crud_mapping.html.erb +125 -0
- data/app/views/lyra/flow/timeline.html.erb +260 -0
- data/app/views/lyra/privacy/pii_detection.html.erb +148 -0
- data/app/views/lyra/privacy/policy.html.erb +188 -0
- data/app/workflows/es_async_mode_workflow.rb +80 -0
- data/app/workflows/es_sync_mode_workflow.rb +64 -0
- data/app/workflows/hijack_mode_workflow.rb +54 -0
- data/app/workflows/lifecycle_workflow.rb +43 -0
- data/app/workflows/monitor_mode_workflow.rb +39 -0
- data/config/privacy_policies.rb +273 -0
- data/config/routes.rb +48 -0
- data/lib/lyra/aggregate.rb +131 -0
- data/lib/lyra/associations/event_aware.rb +225 -0
- data/lib/lyra/command.rb +81 -0
- data/lib/lyra/command_handler.rb +155 -0
- data/lib/lyra/configuration.rb +124 -0
- data/lib/lyra/consistency/read_your_writes.rb +91 -0
- data/lib/lyra/correlation.rb +144 -0
- data/lib/lyra/dual_view.rb +231 -0
- data/lib/lyra/engine.rb +67 -0
- data/lib/lyra/event.rb +71 -0
- data/lib/lyra/event_analyzer.rb +135 -0
- data/lib/lyra/event_flow.rb +449 -0
- data/lib/lyra/event_mapper.rb +106 -0
- data/lib/lyra/event_store_adapter.rb +72 -0
- data/lib/lyra/id_generator.rb +137 -0
- data/lib/lyra/interceptors/association_interceptor.rb +169 -0
- data/lib/lyra/interceptors/crud_interceptor.rb +543 -0
- data/lib/lyra/privacy/gdpr_compliance.rb +161 -0
- data/lib/lyra/privacy/pii_detector.rb +85 -0
- data/lib/lyra/privacy/pii_masker.rb +66 -0
- data/lib/lyra/privacy/policy_integration.rb +253 -0
- data/lib/lyra/projection.rb +94 -0
- data/lib/lyra/projections/async_projection_job.rb +63 -0
- data/lib/lyra/projections/cached_projection.rb +322 -0
- data/lib/lyra/projections/cached_relation.rb +757 -0
- data/lib/lyra/projections/event_store_reader.rb +127 -0
- data/lib/lyra/projections/model_projection.rb +143 -0
- data/lib/lyra/schema/diff.rb +331 -0
- data/lib/lyra/schema/event_class_registrar.rb +63 -0
- data/lib/lyra/schema/generator.rb +190 -0
- data/lib/lyra/schema/reporter.rb +188 -0
- data/lib/lyra/schema/store.rb +156 -0
- data/lib/lyra/schema/validator.rb +100 -0
- data/lib/lyra/strict_data_access.rb +363 -0
- data/lib/lyra/verification/crud_lifecycle_workflow.rb +456 -0
- data/lib/lyra/verification/workflow_generator.rb +540 -0
- data/lib/lyra/version.rb +3 -0
- data/lib/lyra/visualization/activity_heatmap.rb +215 -0
- data/lib/lyra/visualization/event_graph.rb +310 -0
- data/lib/lyra/visualization/timeline.rb +398 -0
- data/lib/lyra.rb +150 -0
- data/lib/tasks/dist.rake +391 -0
- data/lib/tasks/gems.rake +185 -0
- data/lib/tasks/lyra_schema.rake +231 -0
- data/lib/tasks/lyra_workflows.rake +452 -0
- data/lib/tasks/public_release.rake +351 -0
- data/lib/tasks/stats.rake +175 -0
- data/lib/tasks/testbed.rake +479 -0
- data/lib/tasks/version.rake +159 -0
- metadata +221 -0
data/Rakefile
ADDED
|
@@ -0,0 +1,728 @@
|
|
|
1
|
+
require "bundler/setup"
|
|
2
|
+
require "rake/testtask"
|
|
3
|
+
|
|
4
|
+
APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
|
|
5
|
+
# load "rails/tasks/engine.rake" if File.exist?(APP_RAKEFILE)
|
|
6
|
+
|
|
7
|
+
# load "rails/tasks/statistics.rake"
|
|
8
|
+
|
|
9
|
+
require "bundler/gem_tasks"
|
|
10
|
+
|
|
11
|
+
# Load custom tasks
|
|
12
|
+
Dir.glob("lib/tasks/*.rake").each { |r| load r }
|
|
13
|
+
|
|
14
|
+
Rake::TestTask.new(:test) do |t|
|
|
15
|
+
t.libs << "lib"
|
|
16
|
+
t.libs << "test"
|
|
17
|
+
t.test_files = FileList["test/**/*_test.rb"]
|
|
18
|
+
t.verbose = true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
namespace :test do
|
|
23
|
+
desc "Run unit tests only (excludes controller tests)"
|
|
24
|
+
Rake::TestTask.new(:unit) do |t|
|
|
25
|
+
t.libs << "lib"
|
|
26
|
+
t.libs << "test"
|
|
27
|
+
t.test_files = FileList["test/**/*_test.rb"].exclude("test/controllers/**/*_test.rb")
|
|
28
|
+
t.verbose = true
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
desc "Run controller tests only (requires dummy Rails app)"
|
|
32
|
+
Rake::TestTask.new(:controllers) do |t|
|
|
33
|
+
t.libs << "lib"
|
|
34
|
+
t.libs << "test"
|
|
35
|
+
t.test_files = FileList["test/controllers/**/*_test.rb"]
|
|
36
|
+
t.verbose = true
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
desc "Run tests for all components (Lyra, PAM DSL, PetriFlow)"
|
|
40
|
+
task :all do
|
|
41
|
+
puts "\n=== Running Lyra tests ==="
|
|
42
|
+
Rake::Task["test"].invoke
|
|
43
|
+
|
|
44
|
+
puts "\n=== Running PAM DSL tests ==="
|
|
45
|
+
Dir.chdir("gems/pam_dsl") { system("rake test") || raise("PAM DSL tests failed") }
|
|
46
|
+
|
|
47
|
+
puts "\n=== Running PetriFlow tests ==="
|
|
48
|
+
Dir.chdir("gems/petri_flow") { system("rake test") || raise("PetriFlow tests failed") }
|
|
49
|
+
|
|
50
|
+
puts "\n=== All tests passed! ==="
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
task default: :test
|
|
55
|
+
|
|
56
|
+
# =============================================================================
|
|
57
|
+
# Shared constants
|
|
58
|
+
# =============================================================================
|
|
59
|
+
TESTBED_DIR = File.expand_path("examples/aegean_epay_testbed", __dir__)
|
|
60
|
+
|
|
61
|
+
# =============================================================================
|
|
62
|
+
# Benchmark tasks
|
|
63
|
+
# =============================================================================
|
|
64
|
+
namespace :benchmark do
|
|
65
|
+
|
|
66
|
+
desc "Run Lyra mode comparison benchmark (delegates to aegean_epay_testbed)"
|
|
67
|
+
task :modes do
|
|
68
|
+
# Collect all arguments after the task name
|
|
69
|
+
# Usage: rake benchmark:modes -- --scales=1000,5000 --modes=disabled,monitor
|
|
70
|
+
args = ARGV.drop_while { |a| a != "--" }.drop(1).join(" ")
|
|
71
|
+
|
|
72
|
+
Dir.chdir(TESTBED_DIR) do
|
|
73
|
+
# Use bundle exec with testbed's Gemfile to ensure correct dependencies
|
|
74
|
+
cmd = "bundle exec ruby perf/run_mode_comparison_benchmark.rb #{args}"
|
|
75
|
+
puts "Running: RAILS_ENV=test #{cmd}"
|
|
76
|
+
puts "Working directory: #{TESTBED_DIR}"
|
|
77
|
+
puts ""
|
|
78
|
+
env = {
|
|
79
|
+
"RAILS_ENV" => "test",
|
|
80
|
+
"BUNDLE_GEMFILE" => File.join(TESTBED_DIR, "Gemfile")
|
|
81
|
+
}
|
|
82
|
+
system(env, cmd) || exit(1)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Prevent rake from trying to run arguments as tasks
|
|
86
|
+
exit(0)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
desc "Continue interrupted benchmark run"
|
|
90
|
+
task :continue do
|
|
91
|
+
Dir.chdir(TESTBED_DIR) do
|
|
92
|
+
cmd = "bundle exec ruby perf/run_mode_comparison_benchmark.rb --continue"
|
|
93
|
+
puts "Resuming benchmark from checkpoint..."
|
|
94
|
+
puts "Working directory: #{TESTBED_DIR}"
|
|
95
|
+
puts ""
|
|
96
|
+
env = {
|
|
97
|
+
"RAILS_ENV" => "test",
|
|
98
|
+
"BUNDLE_GEMFILE" => File.join(TESTBED_DIR, "Gemfile")
|
|
99
|
+
}
|
|
100
|
+
system(env, cmd) || exit(1)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
desc "Run concurrent operations benchmark (thread scaling)"
|
|
105
|
+
task :concurrent do
|
|
106
|
+
args = ARGV.drop_while { |a| a != "--" }.drop(1).join(" ")
|
|
107
|
+
|
|
108
|
+
Dir.chdir(TESTBED_DIR) do
|
|
109
|
+
cmd = "bundle exec ruby perf/run_concurrent_benchmark.rb #{args}"
|
|
110
|
+
puts "Running: RAILS_ENV=test #{cmd}"
|
|
111
|
+
puts "Working directory: #{TESTBED_DIR}"
|
|
112
|
+
puts ""
|
|
113
|
+
env = {
|
|
114
|
+
"RAILS_ENV" => "test",
|
|
115
|
+
"BUNDLE_GEMFILE" => File.join(TESTBED_DIR, "Gemfile")
|
|
116
|
+
}
|
|
117
|
+
system(env, cmd) || exit(1)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
exit(0)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
desc "Show benchmark help"
|
|
124
|
+
task :help do
|
|
125
|
+
puts <<~HELP
|
|
126
|
+
Lyra Benchmark Tasks
|
|
127
|
+
====================
|
|
128
|
+
|
|
129
|
+
rake benchmark:modes Run mode comparison benchmark (7 modes, 4 scenarios)
|
|
130
|
+
rake benchmark:concurrent Run concurrent/thread scaling benchmark
|
|
131
|
+
rake benchmark:continue Resume interrupted mode comparison run
|
|
132
|
+
rake benchmark:help Show this help
|
|
133
|
+
|
|
134
|
+
Mode Comparison (benchmark:modes):
|
|
135
|
+
Compares all 7 Lyra modes across CRUD, batch, query, and mixed scenarios.
|
|
136
|
+
Supports checkpoint/resume for long-running benchmarks.
|
|
137
|
+
|
|
138
|
+
rake benchmark:modes -- --scales=1000,5000 --modes=disabled,monitor
|
|
139
|
+
rake benchmark:modes -- --scenarios=crud,batch
|
|
140
|
+
rake benchmark:modes -- --help
|
|
141
|
+
|
|
142
|
+
Concurrent Benchmark (benchmark:concurrent):
|
|
143
|
+
Tests throughput under concurrent load with multiple threads.
|
|
144
|
+
|
|
145
|
+
rake benchmark:concurrent -- --threads=1,2,4,8 --operations=100
|
|
146
|
+
rake benchmark:concurrent -- --modes=disabled,monitor,hijack
|
|
147
|
+
|
|
148
|
+
Examples:
|
|
149
|
+
# Quick mode comparison
|
|
150
|
+
rake benchmark:modes -- --scales=100,500 --iterations=10
|
|
151
|
+
|
|
152
|
+
# Full benchmark for paper
|
|
153
|
+
rake benchmark:modes -- --scales=500,1000,5000,10000
|
|
154
|
+
|
|
155
|
+
# Resume after interruption
|
|
156
|
+
rake benchmark:continue
|
|
157
|
+
|
|
158
|
+
# Concurrent scaling test
|
|
159
|
+
rake benchmark:concurrent -- --threads=1,2,4,8,16
|
|
160
|
+
|
|
161
|
+
Note: Ensure PostgreSQL is running before benchmarks (rake docker:start).
|
|
162
|
+
HELP
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# =============================================================================
|
|
167
|
+
# Docker tasks for PostgreSQL container
|
|
168
|
+
# =============================================================================
|
|
169
|
+
namespace :docker do
|
|
170
|
+
CONTAINER_NAME = "lyra_postgres"
|
|
171
|
+
|
|
172
|
+
desc "Ensure PostgreSQL container is running (starts if needed)"
|
|
173
|
+
task :ensure do
|
|
174
|
+
running = `docker ps --filter "name=#{CONTAINER_NAME}" --format "{{.Names}}"`.strip
|
|
175
|
+
if running != CONTAINER_NAME
|
|
176
|
+
Rake::Task["docker:start"].invoke
|
|
177
|
+
else
|
|
178
|
+
# Verify it's healthy
|
|
179
|
+
health = `docker inspect --format='{{.State.Health.Status}}' #{CONTAINER_NAME} 2>/dev/null`.strip
|
|
180
|
+
if health != "healthy"
|
|
181
|
+
print "Waiting for PostgreSQL to be healthy"
|
|
182
|
+
30.times do
|
|
183
|
+
health = `docker inspect --format='{{.State.Health.Status}}' #{CONTAINER_NAME} 2>/dev/null`.strip
|
|
184
|
+
break if health == "healthy"
|
|
185
|
+
print "."
|
|
186
|
+
sleep 1
|
|
187
|
+
end
|
|
188
|
+
puts ""
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
desc "Start PostgreSQL container for testing and benchmarks"
|
|
194
|
+
task :start do
|
|
195
|
+
puts "Starting PostgreSQL container..."
|
|
196
|
+
|
|
197
|
+
# Check if container already exists
|
|
198
|
+
existing = `docker ps -a --filter "name=#{CONTAINER_NAME}" --format "{{.Names}}"`.strip
|
|
199
|
+
|
|
200
|
+
if existing == CONTAINER_NAME
|
|
201
|
+
# Container exists, check if it's running
|
|
202
|
+
running = `docker ps --filter "name=#{CONTAINER_NAME}" --format "{{.Names}}"`.strip
|
|
203
|
+
if running == CONTAINER_NAME
|
|
204
|
+
puts "Container '#{CONTAINER_NAME}' is already running"
|
|
205
|
+
else
|
|
206
|
+
puts "Starting existing container..."
|
|
207
|
+
system("docker start #{CONTAINER_NAME}")
|
|
208
|
+
puts "Container started"
|
|
209
|
+
end
|
|
210
|
+
else
|
|
211
|
+
# Start new container with docker-compose
|
|
212
|
+
puts "Creating new container..."
|
|
213
|
+
system("docker compose up -d")
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Wait for healthcheck
|
|
217
|
+
print "Waiting for PostgreSQL to be ready"
|
|
218
|
+
30.times do
|
|
219
|
+
result = `docker inspect --format='{{.State.Health.Status}}' #{CONTAINER_NAME} 2>/dev/null`.strip
|
|
220
|
+
if result == "healthy"
|
|
221
|
+
puts "\nPostgreSQL is ready on port 5433"
|
|
222
|
+
break
|
|
223
|
+
end
|
|
224
|
+
print "."
|
|
225
|
+
sleep 1
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
desc "Stop PostgreSQL container"
|
|
230
|
+
task :stop do
|
|
231
|
+
puts "Stopping PostgreSQL container..."
|
|
232
|
+
system("docker compose down")
|
|
233
|
+
puts "Container stopped"
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
desc "Show PostgreSQL container status"
|
|
237
|
+
task :status do
|
|
238
|
+
running = `docker ps --filter "name=#{CONTAINER_NAME}" --format "{{.Names}}"`.strip
|
|
239
|
+
if running == CONTAINER_NAME
|
|
240
|
+
puts "Container '#{CONTAINER_NAME}' is running"
|
|
241
|
+
puts ""
|
|
242
|
+
system("docker ps --filter 'name=#{CONTAINER_NAME}' --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'")
|
|
243
|
+
else
|
|
244
|
+
existing = `docker ps -a --filter "name=#{CONTAINER_NAME}" --format "{{.Names}}"`.strip
|
|
245
|
+
if existing == CONTAINER_NAME
|
|
246
|
+
puts "Container '#{CONTAINER_NAME}' exists but is stopped"
|
|
247
|
+
puts " Run 'rake docker:start' to start it"
|
|
248
|
+
else
|
|
249
|
+
puts "Container '#{CONTAINER_NAME}' does not exist"
|
|
250
|
+
puts " Run 'rake docker:start' to create and start it"
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
desc "View PostgreSQL container logs"
|
|
256
|
+
task :logs do
|
|
257
|
+
system("docker logs #{CONTAINER_NAME} --tail 50")
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
desc "Remove PostgreSQL container and data volume"
|
|
261
|
+
task :clean do
|
|
262
|
+
puts "Stopping and removing container..."
|
|
263
|
+
system("docker compose down -v")
|
|
264
|
+
puts "Container and volume removed"
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
desc "Connect to PostgreSQL with psql"
|
|
268
|
+
task :psql do
|
|
269
|
+
system("docker", "exec", "-it", CONTAINER_NAME, "psql", "-U", "postgres")
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# =============================================================================
|
|
274
|
+
# Statistics tasks
|
|
275
|
+
# =============================================================================
|
|
276
|
+
namespace :stats do
|
|
277
|
+
desc "Generate comprehensive code statistics for all components"
|
|
278
|
+
task :all do
|
|
279
|
+
require "json"
|
|
280
|
+
|
|
281
|
+
puts "=" * 70
|
|
282
|
+
puts "LYRA PROJECT CODE STATISTICS"
|
|
283
|
+
puts "=" * 70
|
|
284
|
+
puts "Generated: #{Time.now.strftime("%Y-%m-%d %H:%M:%S")}"
|
|
285
|
+
puts "=" * 70
|
|
286
|
+
puts ""
|
|
287
|
+
|
|
288
|
+
results = {}
|
|
289
|
+
|
|
290
|
+
# Lyra core
|
|
291
|
+
puts "Analyzing Lyra core..."
|
|
292
|
+
results[:lyra] = analyze_directory("lib", "test", "Lyra Core")
|
|
293
|
+
|
|
294
|
+
# PAM DSL
|
|
295
|
+
puts "Analyzing PAM DSL..."
|
|
296
|
+
results[:pam_dsl] = analyze_directory("gems/pam_dsl/lib", "gems/pam_dsl/test", "PAM DSL")
|
|
297
|
+
|
|
298
|
+
# PetriFlow
|
|
299
|
+
puts "Analyzing PetriFlow..."
|
|
300
|
+
results[:petri_flow] = analyze_directory("gems/petri_flow/lib", "gems/petri_flow/test", "PetriFlow")
|
|
301
|
+
|
|
302
|
+
# Aegean E-Pay Testbed
|
|
303
|
+
puts "Analyzing Aegean E-Pay Testbed..."
|
|
304
|
+
results[:testbed] = analyze_rails_app("examples/aegean_epay_testbed", "Aegean E-Pay Testbed")
|
|
305
|
+
|
|
306
|
+
puts ""
|
|
307
|
+
|
|
308
|
+
# Print individual reports
|
|
309
|
+
results.each do |key, data|
|
|
310
|
+
print_component_report(data)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# Print summary
|
|
314
|
+
print_summary(results)
|
|
315
|
+
|
|
316
|
+
# Save to file
|
|
317
|
+
save_report(results)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
desc "Generate statistics for Lyra core only"
|
|
321
|
+
task :lyra do
|
|
322
|
+
result = analyze_directory("lib", "test", "Lyra Core")
|
|
323
|
+
print_component_report(result)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
desc "Generate statistics for PAM DSL"
|
|
327
|
+
task :pam_dsl do
|
|
328
|
+
result = analyze_directory("gems/pam_dsl/lib", "gems/pam_dsl/test", "PAM DSL")
|
|
329
|
+
print_component_report(result)
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
desc "Generate statistics for PetriFlow"
|
|
333
|
+
task :petri_flow do
|
|
334
|
+
result = analyze_directory("gems/petri_flow/lib", "gems/petri_flow/test", "PetriFlow")
|
|
335
|
+
print_component_report(result)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
desc "Generate statistics for Aegean E-Pay Testbed"
|
|
339
|
+
task :testbed do
|
|
340
|
+
result = analyze_rails_app("examples/aegean_epay_testbed", "Aegean E-Pay Testbed")
|
|
341
|
+
print_component_report(result)
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# Helper methods
|
|
345
|
+
def analyze_directory(lib_path, test_path, name)
|
|
346
|
+
lib_stats = count_ruby_files(lib_path)
|
|
347
|
+
test_stats = count_ruby_files(test_path)
|
|
348
|
+
|
|
349
|
+
{
|
|
350
|
+
name: name,
|
|
351
|
+
lib_path: lib_path,
|
|
352
|
+
test_path: test_path,
|
|
353
|
+
code: lib_stats,
|
|
354
|
+
test: test_stats,
|
|
355
|
+
total_files: lib_stats[:files] + test_stats[:files],
|
|
356
|
+
total_lines: lib_stats[:lines] + test_stats[:lines],
|
|
357
|
+
total_loc: lib_stats[:loc] + test_stats[:loc],
|
|
358
|
+
total_classes: lib_stats[:classes] + test_stats[:classes],
|
|
359
|
+
total_methods: lib_stats[:methods] + test_stats[:methods],
|
|
360
|
+
code_to_test_ratio: lib_stats[:loc] > 0 ? (test_stats[:loc].to_f / lib_stats[:loc]).round(2) : 0
|
|
361
|
+
}
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def analyze_rails_app(app_path, name)
|
|
365
|
+
# Analyze different parts of the Rails app
|
|
366
|
+
categories = {
|
|
367
|
+
"Controllers" => "#{app_path}/app/controllers",
|
|
368
|
+
"Models" => "#{app_path}/app/models",
|
|
369
|
+
"Views" => "#{app_path}/app/views",
|
|
370
|
+
"Helpers" => "#{app_path}/app/helpers",
|
|
371
|
+
"Jobs" => "#{app_path}/app/jobs",
|
|
372
|
+
"Mailers" => "#{app_path}/app/mailers",
|
|
373
|
+
"Services" => "#{app_path}/app/services",
|
|
374
|
+
"Repositories" => "#{app_path}/app/repositories",
|
|
375
|
+
"Workflows" => "#{app_path}/app/workflows",
|
|
376
|
+
"Libraries" => "#{app_path}/lib"
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
test_categories = {
|
|
380
|
+
"Controller tests" => "#{app_path}/test/controllers",
|
|
381
|
+
"Model tests" => "#{app_path}/test/models",
|
|
382
|
+
"Integration tests" => "#{app_path}/test/integration",
|
|
383
|
+
"System tests" => "#{app_path}/test/system",
|
|
384
|
+
"Other tests" => "#{app_path}/test"
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
code_stats = { files: 0, lines: 0, loc: 0, classes: 0, methods: 0 }
|
|
388
|
+
test_stats = { files: 0, lines: 0, loc: 0, classes: 0, methods: 0 }
|
|
389
|
+
breakdown = {}
|
|
390
|
+
|
|
391
|
+
categories.each do |category, path|
|
|
392
|
+
if Dir.exist?(path)
|
|
393
|
+
stats = count_ruby_files(path)
|
|
394
|
+
if stats[:files] > 0
|
|
395
|
+
breakdown[category] = stats
|
|
396
|
+
code_stats[:files] += stats[:files]
|
|
397
|
+
code_stats[:lines] += stats[:lines]
|
|
398
|
+
code_stats[:loc] += stats[:loc]
|
|
399
|
+
code_stats[:classes] += stats[:classes]
|
|
400
|
+
code_stats[:methods] += stats[:methods]
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
test_categories.each do |category, path|
|
|
406
|
+
if Dir.exist?(path)
|
|
407
|
+
stats = count_ruby_files(path)
|
|
408
|
+
if stats[:files] > 0
|
|
409
|
+
# Avoid double-counting for "Other tests"
|
|
410
|
+
if category == "Other tests"
|
|
411
|
+
other_files = Dir.glob("#{path}/*_test.rb")
|
|
412
|
+
stats = count_files(other_files)
|
|
413
|
+
end
|
|
414
|
+
if stats[:files] > 0
|
|
415
|
+
breakdown[category] = stats
|
|
416
|
+
test_stats[:files] += stats[:files]
|
|
417
|
+
test_stats[:lines] += stats[:lines]
|
|
418
|
+
test_stats[:loc] += stats[:loc]
|
|
419
|
+
test_stats[:classes] += stats[:classes]
|
|
420
|
+
test_stats[:methods] += stats[:methods]
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
{
|
|
427
|
+
name: name,
|
|
428
|
+
lib_path: app_path,
|
|
429
|
+
test_path: "#{app_path}/test",
|
|
430
|
+
code: code_stats,
|
|
431
|
+
test: test_stats,
|
|
432
|
+
breakdown: breakdown,
|
|
433
|
+
total_files: code_stats[:files] + test_stats[:files],
|
|
434
|
+
total_lines: code_stats[:lines] + test_stats[:lines],
|
|
435
|
+
total_loc: code_stats[:loc] + test_stats[:loc],
|
|
436
|
+
total_classes: code_stats[:classes] + test_stats[:classes],
|
|
437
|
+
total_methods: code_stats[:methods] + test_stats[:methods],
|
|
438
|
+
code_to_test_ratio: code_stats[:loc] > 0 ? (test_stats[:loc].to_f / code_stats[:loc]).round(2) : 0
|
|
439
|
+
}
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def count_ruby_files(path)
|
|
443
|
+
return { files: 0, lines: 0, loc: 0, classes: 0, methods: 0 } unless Dir.exist?(path)
|
|
444
|
+
|
|
445
|
+
files = Dir.glob("#{path}/**/*.rb")
|
|
446
|
+
count_files(files)
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def count_files(files)
|
|
450
|
+
stats = { files: 0, lines: 0, loc: 0, classes: 0, methods: 0 }
|
|
451
|
+
|
|
452
|
+
files.each do |file|
|
|
453
|
+
next unless File.file?(file)
|
|
454
|
+
|
|
455
|
+
stats[:files] += 1
|
|
456
|
+
content = File.read(file)
|
|
457
|
+
lines = content.lines
|
|
458
|
+
|
|
459
|
+
stats[:lines] += lines.size
|
|
460
|
+
stats[:loc] += lines.count { |l| l.strip.length > 0 && !l.strip.start_with?("#") }
|
|
461
|
+
stats[:classes] += content.scan(/^\s*(class|module)\s+\w+/).size
|
|
462
|
+
stats[:methods] += content.scan(/^\s*def\s+\w+/).size
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
stats
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
def print_component_report(data)
|
|
469
|
+
puts "-" * 70
|
|
470
|
+
puts data[:name].upcase
|
|
471
|
+
puts "-" * 70
|
|
472
|
+
puts ""
|
|
473
|
+
|
|
474
|
+
if data[:breakdown]
|
|
475
|
+
puts "+----------------------+--------+--------+---------+---------+"
|
|
476
|
+
puts "| Category | Files | LOC | Classes | Methods |"
|
|
477
|
+
puts "+----------------------+--------+--------+---------+---------+"
|
|
478
|
+
|
|
479
|
+
data[:breakdown].each do |category, stats|
|
|
480
|
+
printf "| %-20s | %6d | %6d | %7d | %7d |\n",
|
|
481
|
+
category, stats[:files], stats[:loc], stats[:classes], stats[:methods]
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
puts "+----------------------+--------+--------+---------+---------+"
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
puts ""
|
|
488
|
+
puts " Code: #{data[:code][:files]} files, #{data[:code][:loc]} LOC, #{data[:code][:classes]} classes, #{data[:code][:methods]} methods"
|
|
489
|
+
puts " Tests: #{data[:test][:files]} files, #{data[:test][:loc]} LOC, #{data[:test][:classes]} classes, #{data[:test][:methods]} methods"
|
|
490
|
+
puts " Total: #{data[:total_files]} files, #{data[:total_loc]} LOC"
|
|
491
|
+
puts " Code to Test Ratio: 1:#{data[:code_to_test_ratio]}"
|
|
492
|
+
puts ""
|
|
493
|
+
end
|
|
494
|
+
|
|
495
|
+
def print_summary(results)
|
|
496
|
+
puts "=" * 70
|
|
497
|
+
puts "COMBINED SUMMARY"
|
|
498
|
+
puts "=" * 70
|
|
499
|
+
puts ""
|
|
500
|
+
|
|
501
|
+
total = {
|
|
502
|
+
code_files: 0, code_loc: 0, code_classes: 0, code_methods: 0,
|
|
503
|
+
test_files: 0, test_loc: 0, test_classes: 0, test_methods: 0
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
puts "+----------------------+--------+--------+---------+---------+--------+--------+"
|
|
507
|
+
puts "| Component | C.Files| C.LOC | Classes | Methods | T.Files| T.LOC |"
|
|
508
|
+
puts "+----------------------+--------+--------+---------+---------+--------+--------+"
|
|
509
|
+
|
|
510
|
+
results.each do |key, data|
|
|
511
|
+
printf "| %-20s | %6d | %6d | %7d | %7d | %6d | %6d |\n",
|
|
512
|
+
data[:name], data[:code][:files], data[:code][:loc],
|
|
513
|
+
data[:code][:classes], data[:code][:methods],
|
|
514
|
+
data[:test][:files], data[:test][:loc]
|
|
515
|
+
|
|
516
|
+
total[:code_files] += data[:code][:files]
|
|
517
|
+
total[:code_loc] += data[:code][:loc]
|
|
518
|
+
total[:code_classes] += data[:code][:classes]
|
|
519
|
+
total[:code_methods] += data[:code][:methods]
|
|
520
|
+
total[:test_files] += data[:test][:files]
|
|
521
|
+
total[:test_loc] += data[:test][:loc]
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
puts "+----------------------+--------+--------+---------+---------+--------+--------+"
|
|
525
|
+
printf "| %-20s | %6d | %6d | %7d | %7d | %6d | %6d |\n",
|
|
526
|
+
"TOTAL", total[:code_files], total[:code_loc],
|
|
527
|
+
total[:code_classes], total[:code_methods],
|
|
528
|
+
total[:test_files], total[:test_loc]
|
|
529
|
+
puts "+----------------------+--------+--------+---------+---------+--------+--------+"
|
|
530
|
+
|
|
531
|
+
puts ""
|
|
532
|
+
puts "Grand Total: #{total[:code_files] + total[:test_files]} files, #{total[:code_loc] + total[:test_loc]} LOC"
|
|
533
|
+
puts "Code to Test Ratio: 1:#{(total[:test_loc].to_f / total[:code_loc]).round(2)}"
|
|
534
|
+
puts ""
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
def save_report(results)
|
|
538
|
+
require "json"
|
|
539
|
+
require "fileutils"
|
|
540
|
+
|
|
541
|
+
report_dir = "examples/aegean_epay_testbed/reports"
|
|
542
|
+
FileUtils.mkdir_p(report_dir)
|
|
543
|
+
|
|
544
|
+
timestamp = Time.now.strftime("%Y%m%d_%H%M%S")
|
|
545
|
+
|
|
546
|
+
# Calculate totals
|
|
547
|
+
total = {
|
|
548
|
+
code_files: 0, code_loc: 0, code_classes: 0, code_methods: 0,
|
|
549
|
+
test_files: 0, test_loc: 0, test_classes: 0, test_methods: 0
|
|
550
|
+
}
|
|
551
|
+
results.each do |_, data|
|
|
552
|
+
total[:code_files] += data[:code][:files]
|
|
553
|
+
total[:code_loc] += data[:code][:loc]
|
|
554
|
+
total[:code_classes] += data[:code][:classes]
|
|
555
|
+
total[:code_methods] += data[:code][:methods]
|
|
556
|
+
total[:test_files] += data[:test][:files]
|
|
557
|
+
total[:test_loc] += data[:test][:loc]
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
# Save JSON
|
|
561
|
+
json_file = "#{report_dir}/stats_#{timestamp}.json"
|
|
562
|
+
File.write(json_file, JSON.pretty_generate({
|
|
563
|
+
generated_at: Time.now.iso8601,
|
|
564
|
+
components: results,
|
|
565
|
+
totals: total
|
|
566
|
+
}))
|
|
567
|
+
|
|
568
|
+
# Save Markdown
|
|
569
|
+
md_file = "#{report_dir}/stats_#{timestamp}.md"
|
|
570
|
+
File.open(md_file, "w") do |f|
|
|
571
|
+
f.puts "# Lyra Project Code Statistics"
|
|
572
|
+
f.puts ""
|
|
573
|
+
f.puts "Generated: #{Time.now.strftime("%Y-%m-%d %H:%M:%S")}"
|
|
574
|
+
f.puts ""
|
|
575
|
+
f.puts "## Summary"
|
|
576
|
+
f.puts ""
|
|
577
|
+
f.puts "| Component | Code Files | Code LOC | Classes | Methods | Test Files | Test LOC | Ratio |"
|
|
578
|
+
f.puts "|-----------|------------|----------|---------|---------|------------|----------|-------|"
|
|
579
|
+
|
|
580
|
+
results.each do |_, data|
|
|
581
|
+
f.puts "| #{data[:name]} | #{data[:code][:files]} | #{data[:code][:loc]} | #{data[:code][:classes]} | #{data[:code][:methods]} | #{data[:test][:files]} | #{data[:test][:loc]} | 1:#{data[:code_to_test_ratio]} |"
|
|
582
|
+
end
|
|
583
|
+
|
|
584
|
+
f.puts "| **TOTAL** | **#{total[:code_files]}** | **#{total[:code_loc]}** | **#{total[:code_classes]}** | **#{total[:code_methods]}** | **#{total[:test_files]}** | **#{total[:test_loc]}** | **1:#{(total[:test_loc].to_f / total[:code_loc]).round(2)}** |"
|
|
585
|
+
f.puts ""
|
|
586
|
+
f.puts "## Component Details"
|
|
587
|
+
f.puts ""
|
|
588
|
+
|
|
589
|
+
results.each do |_, data|
|
|
590
|
+
f.puts "### #{data[:name]}"
|
|
591
|
+
f.puts ""
|
|
592
|
+
f.puts "- **Code:** #{data[:code][:files]} files, #{data[:code][:loc]} LOC, #{data[:code][:classes]} classes, #{data[:code][:methods]} methods"
|
|
593
|
+
f.puts "- **Tests:** #{data[:test][:files]} files, #{data[:test][:loc]} LOC"
|
|
594
|
+
f.puts "- **Path:** `#{data[:lib_path]}`"
|
|
595
|
+
f.puts ""
|
|
596
|
+
|
|
597
|
+
if data[:breakdown]
|
|
598
|
+
f.puts "| Category | Files | LOC | Classes | Methods |"
|
|
599
|
+
f.puts "|----------|-------|-----|---------|---------|"
|
|
600
|
+
data[:breakdown].each do |category, stats|
|
|
601
|
+
f.puts "| #{category} | #{stats[:files]} | #{stats[:loc]} | #{stats[:classes]} | #{stats[:methods]} |"
|
|
602
|
+
end
|
|
603
|
+
f.puts ""
|
|
604
|
+
end
|
|
605
|
+
end
|
|
606
|
+
end
|
|
607
|
+
|
|
608
|
+
puts "Reports saved to:"
|
|
609
|
+
puts " - #{json_file}"
|
|
610
|
+
puts " - #{md_file}"
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
# =============================================================================
|
|
615
|
+
# Demo tasks for running the testbed with sample data
|
|
616
|
+
# =============================================================================
|
|
617
|
+
namespace :demo do
|
|
618
|
+
desc "Start demo server with sample data (seeds DB and starts Rails)"
|
|
619
|
+
task :start do
|
|
620
|
+
mode = ENV.fetch("LYRA_MODE", "monitor")
|
|
621
|
+
port = ENV.fetch("PORT", "3000")
|
|
622
|
+
|
|
623
|
+
Dir.chdir(TESTBED_DIR) do
|
|
624
|
+
env = {
|
|
625
|
+
"RAILS_ENV" => "development",
|
|
626
|
+
"LYRA_MODE" => mode,
|
|
627
|
+
"BUNDLE_GEMFILE" => File.join(TESTBED_DIR, "Gemfile")
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
puts "=" * 60
|
|
631
|
+
puts "LYRA DEMO SERVER"
|
|
632
|
+
puts "=" * 60
|
|
633
|
+
puts ""
|
|
634
|
+
puts "Mode: #{mode}"
|
|
635
|
+
puts "Port: #{port}"
|
|
636
|
+
puts ""
|
|
637
|
+
|
|
638
|
+
# Run seed script
|
|
639
|
+
puts "Seeding demo data..."
|
|
640
|
+
seed_script = File.join(TESTBED_DIR, "db/seeds/demo.rb")
|
|
641
|
+
if File.exist?(seed_script)
|
|
642
|
+
system(env, "bundle exec rails runner #{seed_script}")
|
|
643
|
+
puts ""
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
puts "Starting server..."
|
|
647
|
+
puts ""
|
|
648
|
+
puts "Dashboard: http://localhost:#{port}/lyra/dashboard"
|
|
649
|
+
puts "Dual View: http://localhost:#{port}/lyra/dashboard/compare/Registration/1"
|
|
650
|
+
puts "Timeline: http://localhost:#{port}/lyra/flow/timeline"
|
|
651
|
+
puts ""
|
|
652
|
+
puts "Press Ctrl+C to stop"
|
|
653
|
+
puts "-" * 60
|
|
654
|
+
|
|
655
|
+
exec(env, "bundle exec rails server -p #{port}")
|
|
656
|
+
end
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
desc "Seed demo data only (without starting server)"
|
|
660
|
+
task :seed do
|
|
661
|
+
Dir.chdir(TESTBED_DIR) do
|
|
662
|
+
env = {
|
|
663
|
+
"RAILS_ENV" => "development",
|
|
664
|
+
"LYRA_MODE" => ENV.fetch("LYRA_MODE", "monitor"),
|
|
665
|
+
"BUNDLE_GEMFILE" => File.join(TESTBED_DIR, "Gemfile")
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
seed_script = File.join(TESTBED_DIR, "db/seeds/demo.rb")
|
|
669
|
+
if File.exist?(seed_script)
|
|
670
|
+
puts "Seeding demo data..."
|
|
671
|
+
system(env, "bundle exec rails runner #{seed_script}")
|
|
672
|
+
else
|
|
673
|
+
puts "Error: Seed script not found at #{seed_script}"
|
|
674
|
+
exit 1
|
|
675
|
+
end
|
|
676
|
+
end
|
|
677
|
+
end
|
|
678
|
+
|
|
679
|
+
desc "Reset demo database and reseed"
|
|
680
|
+
task :reset do
|
|
681
|
+
Dir.chdir(TESTBED_DIR) do
|
|
682
|
+
env = {
|
|
683
|
+
"RAILS_ENV" => "development",
|
|
684
|
+
"BUNDLE_GEMFILE" => File.join(TESTBED_DIR, "Gemfile")
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
puts "Resetting database..."
|
|
688
|
+
system(env, "bundle exec rails db:reset")
|
|
689
|
+
|
|
690
|
+
puts ""
|
|
691
|
+
Rake::Task["demo:seed"].invoke
|
|
692
|
+
end
|
|
693
|
+
end
|
|
694
|
+
|
|
695
|
+
desc "Show demo help"
|
|
696
|
+
task :help do
|
|
697
|
+
puts <<~HELP
|
|
698
|
+
Lyra Demo Tasks
|
|
699
|
+
===============
|
|
700
|
+
|
|
701
|
+
rake demo:start Start demo server with sample data
|
|
702
|
+
rake demo:seed Seed demo data only
|
|
703
|
+
rake demo:reset Reset database and reseed
|
|
704
|
+
rake demo:help Show this help
|
|
705
|
+
|
|
706
|
+
Environment Variables:
|
|
707
|
+
LYRA_MODE Set Lyra mode (default: monitor)
|
|
708
|
+
Options: disabled, monitor, hijack, es_sync, es_async, es_disabled
|
|
709
|
+
PORT Server port (default: 3000)
|
|
710
|
+
|
|
711
|
+
Examples:
|
|
712
|
+
# Start in monitor mode (default)
|
|
713
|
+
rake demo:start
|
|
714
|
+
|
|
715
|
+
# Start in event sourcing mode
|
|
716
|
+
LYRA_MODE=es_sync rake demo:start
|
|
717
|
+
|
|
718
|
+
# Start on different port
|
|
719
|
+
PORT=4000 rake demo:start
|
|
720
|
+
|
|
721
|
+
Dashboard URLs:
|
|
722
|
+
http://localhost:3000/lyra/dashboard
|
|
723
|
+
http://localhost:3000/lyra/dashboard/model/Registration
|
|
724
|
+
http://localhost:3000/lyra/dashboard/compare/Registration/1
|
|
725
|
+
http://localhost:3000/lyra/flow/timeline
|
|
726
|
+
HELP
|
|
727
|
+
end
|
|
728
|
+
end
|