diff --git a/.github/workflows/api.yml b/.github/workflows/api.yml index 791a00d..27c2c6e 100644 --- a/.github/workflows/api.yml +++ b/.github/workflows/api.yml @@ -16,24 +16,26 @@ on: # A workflow is defined of one or more jobs that can run sequentially or in parallel jobs: - # This workflow contains a single job called "api_test" api_test: - strategy: - # don't cancel other jobs if one fails - fail-fast: false - # maximum number of jobs that can run simultaneously - max-parallel: 1 - matrix: - os: [ubuntu, macos] - runs-on: ${{ matrix.os }}-latest # Runs on latest builds of matrix OSes + runs-on: ubuntu-latest env: BUNDLE_WITHOUT: production # skip installing production gem (pg) - # Steps represent a sequence of tasks that will be executed as part of the job + + # Redis service container for caching + services: + redis: + image: redis + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v4 - - # Builds on a predefined action that has Ruby installed + - uses: actions/checkout@v6 + - uses: ruby/setup-ruby@v1 with: bundler-cache: true # runs 'bundle install' and caches installed gems automatically @@ -49,7 +51,8 @@ jobs: DB_FILENAME: ${{ secrets.DB_FILENAME }} REPOSTORE_PATH: ${{ secrets.REPOSTORE_PATH }} API_HOST: ${{ secrets.API_HOST }} - CLONE_QUEUE_URL: ${{ secrets.CLONE_QUEUE_URL }} + REDIS_URL: ${{ secrets.REDIS_URL }} + WORKER_QUEUE_URL: ${{ secrets.WORKER_QUEUE_URL }} AWS_REGION: ${{ secrets.AWS_REGION }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} diff --git a/.gitignore b/.gitignore index 8ecbd72..9115917 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .claude/ CLAUDE.md +CLAUDE.local.md _snippets/ .bundle/ config/secrets.yml @@ -7,4 +8,4 @@ coverage/* !coverage/.resultset.json *.db repostore/**/ -_cache/ \ No newline at end of file +spec/logs/ \ No newline at end of file diff --git a/Gemfile b/Gemfile index bc60558..959f262 100644 --- a/Gemfile +++ b/Gemfile @@ -28,9 +28,7 @@ gem 'dry-transaction', '~> 0' gem 'dry-validation', '~> 1.0' # Caching -gem 'rack-cache', '~> 1.13' gem 'redis', '~> 4.8' -gem 'redis-rack-cache', '~> 2.2' # DOMAIN LAYER # Validation diff --git a/Gemfile.lock b/Gemfile.lock index 55494e4..86cb1e6 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,8 +5,8 @@ GEM public_suffix (>= 2.0.2, < 8.0) ast (2.4.3) aws-eventstream (1.4.0) - aws-partitions (1.1190.0) - aws-sdk-core (3.239.2) + aws-partitions (1.1198.0) + aws-sdk-core (3.240.0) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -22,7 +22,7 @@ GEM base64 (0.3.0) bigdecimal (3.3.1) coderay (1.1.3) - concurrent-ruby (1.3.5) + concurrent-ruby (1.3.6) cookiejar (0.3.4) crack (1.0.1) bigdecimal @@ -113,9 +113,9 @@ GEM rake figaro (1.3.0) thor (>= 0.14.0, < 2) - flog (4.8.0) - path_expander (~> 1.0) - ruby_parser (~> 3.1, > 3.1.0) + flog (4.9.1) + path_expander (~> 2.0) + prism (~> 1.7) sexp_processor (~> 4.8) hashdiff (1.2.1) hirb (0.7.3) @@ -130,7 +130,7 @@ GEM http_parser.rb (0.8.0) ice_nine (0.11.2) jmespath (1.6.2) - json (2.17.1) + json (2.18.0) language_server-protocol (3.17.0.5) lint_roller (1.1.0) listen (3.9.0) @@ -142,7 +142,7 @@ GEM logger (1.7.0) method_source (1.1.0) mini_portile2 (2.8.9) - minitest (5.26.2) + minitest (5.27.0) minitest-rg (5.3.0) minitest (~> 5.0) multi_json (1.18.0) @@ -153,11 +153,11 @@ GEM parser (3.3.10.0) ast (~> 2.4.1) racc - path_expander (1.1.3) + path_expander (2.0.0) pg (1.6.2) pg (1.6.2-arm64-darwin) pg (1.6.2-x86_64-linux) - prism (1.6.0) + prism (1.7.0) pry (0.15.2) coderay (~> 1.1) method_source (~> 1.0) @@ -166,8 +166,6 @@ GEM nio4r (~> 2.0) racc (1.8.1) rack (3.2.4) - rack-cache (1.17.0) - rack (>= 0.4) rack-session (0.3.0) rack (>= 3.0.0.beta1) rack-test (2.2.0) @@ -178,11 +176,6 @@ GEM rb-inotify (0.11.1) ffi (~> 1.0) redis (4.8.1) - redis-rack-cache (2.2.1) - rack-cache (>= 1.10, < 2) - redis-store (>= 1.6, < 2) - redis-store (1.11.0) - redis (>= 4, < 6) reek (6.5.0) dry-schema (~> 1.13) logger (~> 1.6) @@ -199,9 +192,9 @@ GEM rexml (3.4.4) roar (1.2.0) representable (~> 3.1) - roda (3.98.0) + roda (3.99.0) rack - rubocop (1.81.7) + rubocop (1.82.0) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -209,7 +202,7 @@ GEM parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.47.1, < 2.0) + rubocop-ast (>= 1.48.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) rubocop-ast (1.48.0) @@ -226,9 +219,6 @@ GEM lint_roller (~> 1.1) rubocop (>= 1.72.1, < 2) ruby-progressbar (1.13.0) - ruby_parser (3.21.1) - racc (~> 1.5) - sexp_processor (~> 4.16) sequel (5.99.0) bigdecimal sexp_processor (4.17.4) @@ -249,7 +239,7 @@ GEM uber (0.1.0) unicode-display_width (3.2.0) unicode-emoji (~> 4.1) - unicode-emoji (4.1.0) + unicode-emoji (4.2.0) vcr (6.3.1) base64 webmock (3.26.1) @@ -260,7 +250,7 @@ GEM base64 websocket-extensions (>= 0.1.0) websocket-extensions (0.1.5) - zeitwerk (2.7.3) + zeitwerk (2.7.4) PLATFORMS arm64-darwin-24 @@ -290,12 +280,10 @@ DEPENDENCIES pg (~> 1.0) pry puma (~> 6.0) - rack-cache (~> 1.13) rack-session (~> 0) rack-test rake (~> 13.0) redis (~> 4.8) - redis-rack-cache (~> 2.2) reek rerun roar (~> 1.1) diff --git a/Procfile b/Procfile index 7b12f85..6437772 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,3 @@ release: rake db:migrate; rake queues:create web: bundle exec puma -t 5:5 -p ${PORT:-3000} -e ${RACK_ENV:-development} -worker: bundle exec shoryuken -r ./workers/git_clone_worker.rb -C ./workers/shoryuken.yml \ No newline at end of file +worker: bundle exec shoryuken -r ./workers/application/controllers/worker.rb -C ./workers/shoryuken.yml \ No newline at end of file diff --git a/README.md b/README.md index 7968a0b..448ae24 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,87 @@ # CodePraise Web API -Web API that allowsGithub *projects* to be *appraised* for inidividual *contributions* by *members* of a team. +Web API that allows Github *projects* to be *appraised* for individual *contributions* by *members* of a team. + +## Setup + +### Prerequisites + +1. **Ruby** - See `.ruby-version` for required version +2. **Redis** - Required for caching appraisal results +3. **AWS Account** - For SQS message queue (worker communication) + +### Install Redis + +**Using rake tasks (recommended):** + +```bash +rake redis:start # Start Redis Docker container +rake redis:stop # Stop Redis when done +rake redis:status # Check connectivity +``` + +**Or manually with Homebrew (macOS):** + +```bash +brew install redis +brew services start redis +``` + +### Install Dependencies + +```bash +bundle install +``` + +### Configure Secrets + +```bash +cp config/secrets_example.yml config/secrets.yml +``` + +Edit `config/secrets.yml` and add your: + +- `GITHUB_TOKEN` - GitHub personal access token +- AWS credentials for SQS queue + +### Setup Database + +```bash +bundle exec rake db:migrate # Development database +RACK_ENV=test bundle exec rake db:migrate # Test database +``` + +### Verify Setup + +```bash +rake redis:status # Check Redis connectivity +rake queues:status # Check SQS queue status +``` + +## Running the Application + +```bash +rake run # Start API server on port 9090 +rake worker:run:dev # Start background worker (in separate terminal) +``` + +## Testing + +```bash +rake spec # Run unit and integration tests +bash spec/acceptance_tests # Run full acceptance tests (starts worker automatically) +``` + +## Architecture + +This project uses **Clean Architecture** with separate DDD-style layers for the API and background worker: + +- **API (`app/`)**: Handles HTTP requests, database, GitHub integration +- **Worker (`workers/`)**: Handles git clone/blame operations, Redis caching + +Both use parallel layer structures: `domain/`, `infrastructure/`, `presentation/`, `application/` + +See `CLAUDE.md` for detailed architecture documentation. ## Routes diff --git a/Rakefile b/Rakefile index e87656a..9003c6a 100644 --- a/Rakefile +++ b/Rakefile @@ -8,11 +8,14 @@ task :default do end desc 'Run unit and integration tests' -Rake::TestTask.new(:spec) do |t| +Rake::TestTask.new(:spec_only) do |t| t.pattern = 'spec/tests/**/*_spec.rb' t.warning = false end +# Run specs with cache check +task spec: ['cache:ensure', :spec_only] + desc 'Keep rerunning unit/integration tests upon changes' task :respec do sh "rerun -c 'rake spec' --ignore 'coverage/*' --ignore 'repostore/*'" @@ -35,7 +38,7 @@ end desc 'Keep restarting web app in dev mode upon changes' task :rerun do - sh "rerun -c --ignore 'coverage/*' --ignore 'repostore/*' --ignore '_cache/*' -- bundle exec puma -p 9090" + sh "rerun -c --ignore 'coverage/*' --ignore 'repostore/*'' -- bundle exec puma -p 9090" end namespace :db do @@ -109,54 +112,131 @@ namespace :repos do end namespace :cache do + REDIS_CONTAINER = 'redis-codepraise' + task :config do # rubocop:disable Rake/Desc - require_relative 'app/infrastructure/cache/local_cache' - require_relative 'app/infrastructure/cache/redis_cache' - require_relative 'config/environment' # load config info + require 'redis' + require_relative 'config/environment' + require_relative 'app/infrastructure/cache/remote_cache' @api = CodePraise::App end - desc 'Directory listing of local dev cache' - namespace :list do - desc 'Lists development cache' - task :dev => :config do - puts 'Lists development cache' - keys = CodePraise::Cache::Local.new(@api.config).keys - puts 'No local cache found' if keys.none? - keys.each { |key| puts "Key: #{key}" } + desc 'Check cache server connectivity' + task status: :config do + redis_url = @api.config.REDIS_URL + puts "Environment: #{@api.environment}" + puts "Checking cache at: #{redis_url}" + redis = Redis.new(url: redis_url) + response = redis.ping + puts "Cache responded: #{response}" + puts 'Cache connection successful!' + rescue Redis::CannotConnectError => e + puts "Cache connection FAILED: #{e.message}" + puts '' + puts 'To start Redis locally:' + puts ' rake cache:redis:start' + exit 1 + end + + desc 'Ensure cache is running (start if needed)' + task :ensure do + require 'redis' + require_relative 'config/environment' + redis_url = CodePraise::App.config.REDIS_URL + + redis = Redis.new(url: redis_url) + redis.ping + puts 'Cache is running' + rescue Redis::CannotConnectError + puts 'Cache not running, starting Redis container...' + Rake::Task['cache:redis:start'].invoke + end + + desc 'List all cached keys' + task list: :config do + puts "Environment: #{@api.environment}" + keys = CodePraise::Cache::Remote.new(@api.config).keys + if keys.none? + puts 'No keys found' + else + keys.each { |key| puts " #{key}" } end + end - desc 'Lists production cache' - task :production => :config do - puts 'Finding production cache' - keys = CodePraise::Cache::Remote.new(@api.config).keys - puts 'No keys found' if keys.none? - keys.each { |key| puts "Key: #{key}" } + desc 'Wipe all cached keys' + task wipe: :config do + env = @api.environment + if env == :production + print 'Are you sure you wish to wipe the PRODUCTION cache? (y/n) ' + return unless $stdin.gets.chomp.downcase == 'y' + end + + puts "Wiping #{env} cache..." + wiped = CodePraise::Cache::Remote.new(@api.config).wipe + if wiped.empty? + puts 'No keys to wipe' + else + wiped.each { |key| puts " Wiped: #{key}" } end end - namespace :wipe do - desc 'Delete development cache' - task :dev => :config do - puts 'Deleting development cache' - CodePraise::Cache::Local.new(@api.config).wipe - puts 'Development cache wiped' + # Redis-specific container management (for local development) + namespace :redis do + desc 'Start Redis Docker container' + task :start do + # Check if container exists + container_exists = system("docker ps -a --format '{{.Names}}' | grep -q '^#{REDIS_CONTAINER}$'") + + if container_exists + # Container exists, check if running + container_running = system("docker ps --format '{{.Names}}' | grep -q '^#{REDIS_CONTAINER}$'") + if container_running + puts "Redis container '#{REDIS_CONTAINER}' is already running" + else + puts "Starting existing Redis container '#{REDIS_CONTAINER}'..." + sh "docker start #{REDIS_CONTAINER}" + end + else + # Create and start new container + puts "Creating and starting Redis container '#{REDIS_CONTAINER}'..." + sh "docker run -d --name #{REDIS_CONTAINER} -p 6379:6379 redis:latest" + end + + # Wait for Redis to be ready + puts 'Waiting for Redis to be ready...' + sleep 2 + Rake::Task['cache:status'].invoke end - desc 'Delete production cache' - task :production => :config do - print 'Are you sure you wish to wipe the production cache? (y/n) ' - if $stdin.gets.chomp.downcase == 'y' - puts 'Deleting production cache' - wiped = CodePraise::Cache::Remote.new(@api.config).wipe - wiped.each { |key| puts "Wiped: #{key}" } + desc 'Stop Redis Docker container' + task :stop do + container_running = system("docker ps --format '{{.Names}}' | grep -q '^#{REDIS_CONTAINER}$'") + if container_running + puts "Stopping Redis container '#{REDIS_CONTAINER}'..." + sh "docker stop #{REDIS_CONTAINER}" + puts 'Redis container stopped' + else + puts "Redis container '#{REDIS_CONTAINER}' is not running" + end + end + + desc 'Remove Redis Docker container' + task :remove do + Rake::Task['cache:redis:stop'].invoke + container_exists = system("docker ps -a --format '{{.Names}}' | grep -q '^#{REDIS_CONTAINER}$'") + if container_exists + puts "Removing Redis container '#{REDIS_CONTAINER}'..." + sh "docker rm #{REDIS_CONTAINER}" + puts 'Redis container removed' + else + puts "Redis container '#{REDIS_CONTAINER}' does not exist" end end end end namespace :queues do - task :config do + task :config do # rubocop:disable Rake/Desc require 'aws-sdk-sqs' require_relative 'config/environment' # load config info @api = CodePraise::App @@ -165,26 +245,28 @@ namespace :queues do secret_access_key: @api.config.AWS_SECRET_ACCESS_KEY, region: @api.config.AWS_REGION ) - @q_name = @api.config.CLONE_QUEUE - @q_url = @sqs.get_queue_url(queue_name: @q_name).queue_url - + @q_name = @api.config.WORKER_QUEUE puts "Environment: #{@api.environment}" end + task :get_url => :config do # rubocop:disable Rake/Desc + @q_url = @sqs.get_queue_url(queue_name: @q_name).queue_url + end + desc 'Create SQS queue for worker' task :create => :config do - @sqs.create_queue(queue_name: @q_name) + result = @sqs.create_queue(queue_name: @q_name) puts 'Queue created:' puts " Name: #{@q_name}" puts " Region: #{@api.config.AWS_REGION}" - puts " URL: #{@q_url}" + puts " URL: #{result.queue_url}" rescue StandardError => e puts "Error creating queue: #{e}" end desc 'Report status of queue for worker' - task :status => :config do + task :status => :get_url do puts 'Queue info:' puts " Name: #{@q_name}" puts " Region: #{@api.config.AWS_REGION}" @@ -194,7 +276,7 @@ namespace :queues do end desc 'Purge messages in SQS queue for worker' - task :purge => :config do + task :purge => :get_url do @sqs.purge_queue(queue_url: @q_url) puts "Queue #{@q_name} purged" rescue StandardError => e @@ -206,17 +288,17 @@ namespace :worker do namespace :run do desc 'Run the background cloning worker in development mode' task :dev => :config do - sh 'RACK_ENV=development bundle exec shoryuken -r ./workers/git_clone_worker.rb -C ./workers/shoryuken_dev.yml' + sh 'RACK_ENV=development bundle exec shoryuken -r ./workers/application/controllers/worker.rb -C ./workers/shoryuken_dev.yml' end desc 'Run the background cloning worker in testing mode' task :test => :config do - sh 'RACK_ENV=test bundle exec shoryuken -r ./workers/git_clone_worker.rb -C ./workers/shoryuken_test.yml' + sh 'RACK_ENV=test bundle exec shoryuken -r ./workers/application/controllers/worker.rb -C ./workers/shoryuken_test.yml' end desc 'Run the background cloning worker in production mode' task :production => :config do - sh 'RACK_ENV=production bundle exec shoryuken -r ./workers/git_clone_worker.rb -C ./workers/shoryuken.yml' + sh 'RACK_ENV=production bundle exec shoryuken -r ./workers/application/controllers/worker.rb -C ./workers/shoryuken.yml' end end end diff --git a/app/application/controllers/app.rb b/app/application/controllers/app.rb index 90f1862..729269c 100644 --- a/app/application/controllers/app.rb +++ b/app/application/controllers/app.rb @@ -32,33 +32,30 @@ class App < Roda routing.on String, String do |owner_name, project_name| # GET /projects/{owner_name}/{project_name}[/folder_namepath/] routing.get do - App.configure :production do - response.cache_control public: true, max_age: 300 - end - + # Appraisal results cached in Redis by worker (1-day TTL) request_id = [request.env, request.path, Time.now.to_f].hash path_request = Request::ProjectPath.new( owner_name, project_name, request ) - result = Service::AppraiseProject.new.call( + result = Service::FetchOrRequestAppraisal.new.call( requested: path_request, - request_id: request_id, + request_id:, config: App.config ) if result.failure? + # Failure includes appraisal request being processed by worker failed = Representer::HttpResponse.new(result.failure) routing.halt failed.http_status_code, failed.to_json end - http_response = Representer::HttpResponse.new(result.value!) - response.status = http_response.http_status_code + # Cache hit - return pre-serialized JSON directly + appraisal_result = result.value! + response.status = appraisal_result[:cache_hit] ? 200 : 500 - Representer::ProjectFolderContributions.new( - result.value!.message - ).to_json + appraisal_result[:cached_json] end # POST /projects/{owner_name}/{project_name} diff --git a/app/application/services/appraise_project.rb b/app/application/services/appraise_project.rb deleted file mode 100644 index 2582504..0000000 --- a/app/application/services/appraise_project.rb +++ /dev/null @@ -1,96 +0,0 @@ -# frozen_string_literal: true - -require 'dry/transaction' - -module CodePraise - module Service - # Analyzes contributions to a project - class AppraiseProject - include Dry::Transaction - - step :find_project_details - step :check_project_eligibility - step :request_cloning_worker - step :appraise_contributions - - private - - # rubocop:disable Lint/UselessConstantScoping - NO_PROJ_ERR = 'Project not found' - DB_ERR = 'Having trouble accessing the database' - CLONE_ERR = 'Could not clone this project' - TOO_LARGE_ERR = 'Project is too large to analyze' - NO_FOLDER_ERR = 'Could not find that folder' - PROCESSING_MSG = 'Processing the appraisal request' - # rubocop:enable Lint/UselessConstantScoping - - # input hash keys expected: :project, :requested, :config - def find_project_details(input) - input[:project] = Repository::For.klass(Entity::Project).find_full_name( - input[:requested].owner_name, input[:requested].project_name - ) - - if input[:project] - Success(input) - else - Failure(Response::ApiResult.new(status: :not_found, message: NO_PROJ_ERR)) - end - rescue StandardError - Failure(Response::ApiResult.new(status: :internal_error, message: DB_ERR)) - end - - def check_project_eligibility(input) - if input[:project].too_large? - Failure(Response::ApiResult.new(status: :forbidden, message: TOO_LARGE_ERR)) - else - input[:gitrepo] = GitRepo.new(input[:project], input[:config]) - Success(input) - end - end - - def request_cloning_worker(input) - return Success(input) if input[:gitrepo].exists_locally? - - Messaging::Queue.new(App.config.CLONE_QUEUE_URL, App.config) - .send(clone_request_json(input)) - - Failure(Response::ApiResult.new( - status: :processing, - message: { request_id: input[:request_id], msg: PROCESSING_MSG } - )) - rescue StandardError => e - log_error(e) - Failure(Response::ApiResult.new(status: :internal_error, message: CLONE_ERR)) - end - - def appraise_contributions(input) - input[:folder] = Mapper::Contributions - .new(input[:gitrepo]).for_folder(input[:requested].folder_name) - - appraisal = Response::ProjectFolderContributions.new(input[:project], input[:folder]) - Success(Response::ApiResult.new(status: :ok, message: appraisal)) - rescue StandardError - # App.logger.error "Could not find: #{full_request_path(input)}" - Failure(Response::ApiResult.new(status: :not_found, message: NO_FOLDER_ERR)) - end - - # Helper methods for steps - - def full_request_path(input) - [input[:requested].owner_name, - input[:requested].project_name, - input[:requested].folder_name].join('/') - end - - def log_error(error) - App.logger.error [error.inspect, error.backtrace].flatten.join("\n") - end - - def clone_request_json(input) - Response::CloneRequest.new(input[:project], input[:request_id]) - .then { Representer::CloneRequest.new(_1) } - .then(&:to_json) - end - end - end -end diff --git a/app/application/services/fetch_or_request_appraisal.rb b/app/application/services/fetch_or_request_appraisal.rb new file mode 100644 index 0000000..f9e6132 --- /dev/null +++ b/app/application/services/fetch_or_request_appraisal.rb @@ -0,0 +1,103 @@ +# frozen_string_literal: true + +require 'dry/transaction' + +module CodePraise + module Service + # Fetches appraisal from cache or requests worker to create it + # Does NOT perform appraisal - that's delegated to the worker + class FetchOrRequestAppraisal + include Dry::Transaction + + step :find_project_details + step :check_project_eligibility + step :check_cache + step :request_appraisal_worker + + private + + NO_PROJ_ERR = 'Project not found' + DB_ERR = 'Having trouble accessing the database' + REQUEST_ERR = 'Could not request appraisal' + TOO_LARGE_ERR = 'Project is too large to analyze' + PROCESSING_MSG = 'Processing the appraisal request' + + # input hash keys expected: :requested, :request_id, :config + def find_project_details(input) + input[:project] = Repository::For.klass(Entity::Project).find_full_name( + input[:requested].owner_name, input[:requested].project_name + ) + + if input[:project] + Success(input) + else + Failure(Response::ApiResult.new(status: :not_found, message: NO_PROJ_ERR)) + end + rescue StandardError + Failure(Response::ApiResult.new(status: :internal_error, message: DB_ERR)) + end + + def check_project_eligibility(input) + if input[:project].too_large? + Failure(Response::ApiResult.new(status: :forbidden, message: TOO_LARGE_ERR)) + else + Success(input) + end + end + + def check_cache(input) + cache = Cache::Remote.new(input[:config]) + cache_key = appraisal_cache_key(input) + + cached_json = cache.get(cache_key) + return Success(input) unless cached_json + + # Cache hit - return the cached JSON directly + # Mark as cache hit so controller knows to pass through + input[:cached_json] = cached_json + input[:cache_hit] = true + Success(input) + rescue StandardError => e + # Cache errors should not fail the request - continue to worker + App.logger.warn "Cache error: #{e.message}" + Success(input) + end + + def request_appraisal_worker(input) + # If cache hit, we're done - return success with cached data + return Success(input) if input[:cache_hit] + + # Cache miss - send request to worker + Messaging::Queue.new(App.config.WORKER_QUEUE_URL, App.config) + .send(appraisal_request_json(input)) + + Failure(Response::ApiResult.new( + status: :processing, + message: { request_id: input[:request_id], msg: PROCESSING_MSG } + )) + rescue StandardError => e + log_error(e) + Failure(Response::ApiResult.new(status: :internal_error, message: REQUEST_ERR)) + end + + # Helper methods + + def appraisal_cache_key(input) + folder_path = input[:requested].folder_name || '' + "appraisal:#{input[:project].fullname}/#{folder_path}" + end + + def appraisal_request_json(input) + Response::AppraisalRequest.new( + input[:project], + input[:requested].folder_name || '', + input[:request_id] + ).then { Representer::AppraisalRequest.new(it).to_json } + end + + def log_error(error) + App.logger.error [error.inspect, error.backtrace].flatten.join("\n") + end + end + end +end diff --git a/app/domain/contributions/values/appraisal.rb b/app/domain/contributions/values/appraisal.rb new file mode 100644 index 0000000..5137f53 --- /dev/null +++ b/app/domain/contributions/values/appraisal.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'dry-types' +require 'dry-struct' + +# Require project entity (always available in API) +require_relative '../../projects/entities/project' + +# Note: FolderContributions is loaded by the worker via workers/domain/contributions/ +# The folder attribute uses duck typing - any object responding to expected methods + +module CodePraise + module Value + # Value object representing the result of appraising a project folder + # Immutable snapshot of appraisal outcome (success or error) + class Appraisal < Dry::Struct + include Dry.Types + + # Cache TTL constants + SUCCESS_TTL = 86_400 # 1 day in seconds + ERROR_TTL = 10 # 10 seconds + + # Status must be :ok or :error + attribute :status, Strict::Symbol.enum(:ok, :error) + + # Project is always required (needed for cache key) + attribute :project, Instance(Entity::Project) + + # Folder path being appraised (empty string for root) + attribute :folder_path, Strict::String + + # Folder contributions (present on success, nil on error) + # Uses Nominal type to accept any FolderContributions-like object + attribute :folder, Nominal(Object).optional + + # Error details (present on error, nil on success) + attribute :error_type, Strict::String.optional + attribute :error_message, Strict::String.optional + + def success? + status == :ok + end + + def error? + status == :error + end + + def cache_key + "appraisal:#{project.fullname}/#{folder_path}" + end + + def ttl + success? ? SUCCESS_TTL : ERROR_TTL + end + + # Factory method for successful appraisal + def self.success(project:, folder_path:, folder:) + new( + status: :ok, + project:, + folder_path:, + folder:, + error_type: nil, + error_message: nil + ) + end + + # Factory method for failed appraisal + def self.error(project:, folder_path:, error_type:, error_message:) + new( + status: :error, + project:, + folder_path:, + folder: nil, + error_type:, + error_message: + ) + end + end + end +end diff --git a/app/infrastructure/cache/local_cache.rb b/app/infrastructure/cache/local_cache.rb deleted file mode 100644 index a9f6d8b..0000000 --- a/app/infrastructure/cache/local_cache.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -require 'fileutils' - -module CodePraise - module Cache - # Local disk cache utility - class Local - def initialize(config) - @cache_dir = config.LOCAL_CACHE - ensure_cache_directory - end - - def keys - Dir.glob("#{@cache_dir}/**/*").select { |f| File.file?(f) } - end - - def wipe - FileUtils.rm_rf(Dir.glob("#{@cache_dir}/*")) - end - - private - - def ensure_cache_directory - FileUtils.mkdir_p(@cache_dir) - end - end - end -end diff --git a/app/infrastructure/cache/redis_cache.rb b/app/infrastructure/cache/redis_cache.rb deleted file mode 100644 index 2ba5cef..0000000 --- a/app/infrastructure/cache/redis_cache.rb +++ /dev/null @@ -1,22 +0,0 @@ -# frozen_string_literal: true - -require 'redis' - -module CodePraise - module Cache - # Redis client utility - class Remote - def initialize(config) - @redis = Redis.new(url: config.REDISCLOUD_URL) - end - - def keys - @redis.keys - end - - def wipe - keys.each { |key| @redis.del(key) } - end - end - end -end diff --git a/app/infrastructure/cache/remote_cache.rb b/app/infrastructure/cache/remote_cache.rb new file mode 100644 index 0000000..83000de --- /dev/null +++ b/app/infrastructure/cache/remote_cache.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'redis' + +module CodePraise + module Cache + # Redis client utility for caching appraisal results with TTL support + # Environment isolation via separate Redis databases (configured in secrets.yml): + # - Development: redis://localhost:6379/0 + # - Test: redis://localhost:6379/1 + # - Production: as assigned by provider + class Remote + def initialize(config) + @redis = Redis.new(url: config.REDIS_URL) + end + + # Store a value with expiration + # @param key [String] cache key + # @param value [String] value to store (caller handles serialization) + # @param ttl [Integer] time-to-live in seconds + def set(key, value, ttl:) + @redis.setex(key, ttl, value) + end + + # Retrieve a cached value + # @param key [String] cache key + # @return [String, nil] cached value or nil if not found/expired + def get(key) + @redis.get(key) + end + + # Check if a key exists + # @param key [String] cache key + # @return [Boolean] true if key exists + def exists?(key) + @redis.exists?(key) + end + + def keys + @redis.keys('*') + end + + def wipe + keys.each { |key| @redis.del(key) } + end + end + end +end diff --git a/app/infrastructure/messaging/queue.rb b/app/infrastructure/messaging/queue.rb index 8980d1b..8194dea 100644 --- a/app/infrastructure/messaging/queue.rb +++ b/app/infrastructure/messaging/queue.rb @@ -21,7 +21,7 @@ def initialize(queue_url, config) ## Sends message to queue # Usage: - # q = Messaging::Queue.new(App.config.CLONE_QUEUE_URL) + # q = Messaging::Queue.new(App.config.WORKER_QUEUE_URL) # q.send({data: "hello"}.to_json) def send(message) @queue.send_message(message_body: message) @@ -29,7 +29,7 @@ def send(message) ## Polls queue, yielding each messge # Usage: - # q = Messaging::Queue.new(App.config.CLONE_QUEUE_URL) + # q = Messaging::Queue.new(App.config.WORKER_QUEUE_URL) # q.poll { |msg| print msg.body.to_s } def poll poller = Aws::SQS::QueuePoller.new(@queue_url) diff --git a/app/presentation/representers/appraisal_representer.rb b/app/presentation/representers/appraisal_representer.rb new file mode 100644 index 0000000..03ff0e4 --- /dev/null +++ b/app/presentation/representers/appraisal_representer.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'ostruct' +require 'roar/decorator' +require 'roar/json' + +require_relative 'project_representer' +require_relative 'folder_contributions_representer' + +module CodePraise + module Representer + # Represents appraisal result with status wrapper + # Success: { "status": "ok", "project": {...}, "folder": {...} } + # Error: { "status": "error", "project": {...}, "error_type": "...", "message": "..." } + class Appraisal < Roar::Decorator + include Roar::JSON + + property :status, exec_context: :decorator + property :project, extend: Representer::Project, class: OpenStruct + property :folder_path + + # Success case: include folder contributions + property :folder, extend: Representer::FolderContributions, class: OpenStruct, + if: ->(represented:, **) { represented.success? } + + # Error case: include error details + property :error_type, if: ->(represented:, **) { represented.error? } + property :error_message, as: :message, if: ->(represented:, **) { represented.error? } + + # Convert symbol status to string for JSON + def status + represented.status.to_s + end + end + end +end diff --git a/app/presentation/representers/project_folder_contributions_representer.rb b/app/presentation/representers/appraisal_request_representer.rb similarity index 50% rename from app/presentation/representers/project_folder_contributions_representer.rb rename to app/presentation/representers/appraisal_request_representer.rb index cbf599a..31b937a 100644 --- a/app/presentation/representers/project_folder_contributions_representer.rb +++ b/app/presentation/representers/appraisal_request_representer.rb @@ -1,20 +1,19 @@ # frozen_string_literal: true -require 'ostruct' require 'roar/decorator' require 'roar/json' - -require_relative 'folder_contributions_representer' require_relative 'project_representer' module CodePraise module Representer - # Represents folder summary about repo's folder - class ProjectFolderContributions < Roar::Decorator + # Representer for appraisal request sent to worker queue + # Includes full project info so worker doesn't need database access + class AppraisalRequest < Roar::Decorator include Roar::JSON property :project, extend: Representer::Project, class: OpenStruct - property :folder, extend: Representer::FolderContributions, class: OpenStruct + property :folder_path + property :id end end end diff --git a/app/presentation/responses/clone_request.rb b/app/presentation/responses/clone_request.rb index a61b8a3..e77e66f 100644 --- a/app/presentation/responses/clone_request.rb +++ b/app/presentation/responses/clone_request.rb @@ -2,6 +2,11 @@ module CodePraise module Response + # Request to clone a project (legacy - for backwards compatibility) CloneRequest = Struct.new :project, :id + + # Request to appraise a project folder + # Includes full project info so worker doesn't need database access + AppraisalRequest = Struct.new :project, :folder_path, :id end end diff --git a/app/presentation/responses/project_folder_contributions.rb b/app/presentation/responses/project_folder_contributions.rb deleted file mode 100644 index 3e97bde..0000000 --- a/app/presentation/responses/project_folder_contributions.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -module CodePraise - module Response - # Contributions for a folder of a project - ProjectFolderContributions = Struct.new(:project, :folder) - end -end diff --git a/config/environment.rb b/config/environment.rb index 1a3ee2b..4f1cc69 100644 --- a/config/environment.rb +++ b/config/environment.rb @@ -5,8 +5,6 @@ require 'rack/session' require 'roda' require 'sequel' -require 'rack/cache' -require 'redis-rack-cache' module CodePraise # Environment-specific configuration @@ -25,24 +23,6 @@ def self.config = Figaro.env plugin :common_logger, $stderr end - # Setup Cacheing mechanism - configure :development do - use Rack::Cache, - verbose: true, - metastore: "#{config.LOCAL_CACHE}/meta", - entitystore: "#{config.LOCAL_CACHE}/body" - end - - configure :production do - puts 'RUNNING IN PRODUCTION MODE' - # Set DATABASE_URL environment variable on production platform - - use Rack::Cache, - verbose: true, - metastore: "#{config.REDISCLOUD_URL}/0/metastore", - entitystore: "#{config.REDISCLOUD_URL}/0/entitystore" - end - # Automated HTTP stubbing for testing only configure :app_test do require_relative '../spec/helpers/vcr_helper' diff --git a/config/secrets_example.yml b/config/secrets_example.yml index c80db59..350d2db 100644 --- a/config/secrets_example.yml +++ b/config/secrets_example.yml @@ -1,58 +1,54 @@ --- # Name your file secrets.yml +# +# Redis database indexes for environment isolation: +# - Development: /0 (default) +# - Test: /1 +# - Production: as assigned by provider (usually /0) development: DB_FILENAME: db/local/dev.db GITHUB_TOKEN: create_github_token REPOSTORE_PATH: repostore - LOCAL_CACHE: _cache/rack API_HOST: http://localhost:9090 - REDISCLOUD_URL: url-assigned-by-Redis-provider-on-Heroku + REDIS_URL: redis://localhost:6379/0 AWS_ACCESS_KEY_ID: AWS_SECRET_ACCESS_KEY: AWS_REGION: - CLONE_QUEUE: soa-codepraise-clone-dev - CLONE_QUEUE_URL: - REPORT_QUEUE: codepraise-report-development - REPORT_QUEUE_URL: app_test: DB_FILENAME: db/local/test.db REPOSTORE_PATH: repostore - LOCAL_CACHE: _cache/rack API_HOST: http://localhost:9090 - REDISCLOUD_URL: url-assigned-by-Redis-provider-on-Heroku + REDIS_URL: redis://localhost:6379/1 AWS_ACCESS_KEY_ID: AWS_SECRET_ACCESS_KEY: AWS_REGION: - CLONE_QUEUE: codepraise-clone-test - CLONE_QUEUE_URL: - REPORT_QUEUE: codepraise-report-test - REPORT_QUEUE_URL: test: DB_FILENAME: db/local/test.db GITHUB_TOKEN: create_github_token REPOSTORE_PATH: repostore - LOCAL_CACHE: _cache/rack API_HOST: http://localhost:9090 - REDISCLOUD_URL: url-assigned-by-Redis-provider-on-Heroku + REDIS_URL: redis://localhost:6379/1 AWS_ACCESS_KEY_ID: AWS_SECRET_ACCESS_KEY: AWS_REGION: - CLONE_QUEUE: codepraise-clone-test - CLONE_QUEUE_URL: - REPORT_QUEUE: codepraise-report-test - REPORT_QUEUE_URL: production: # - assign DATABASE_URL in production GITHUB_TOKEN: create_github_token REPOSTORE_PATH: repostore API_HOST: https://codepraise2022-api - REDISCLOUD_URL: url-assigned-by-Redis-provider-on-Heroku + REDIS_URL: AWS_ACCESS_KEY_ID: AWS_SECRET_ACCESS_KEY: AWS_REGION: - CLONE_QUEUE: codepraise-clone-production - CLONE_QUEUE_URL: + WORKER_QUEUE: codepraise-worker-production + WORKER_QUEUE_URL: diff --git a/coverage/.resultset.json b/coverage/.resultset.json index 1d6d286..bde9e12 100644 --- a/coverage/.resultset.json +++ b/coverage/.resultset.json @@ -16,56 +16,61 @@ 1, null, 1, - 60, + 38, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/config/environment.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/require_worker.rb": { "lines": [ null, null, - 1, - 1, - 1, - 1, - 1, - 1, - 1, null, - 1, null, - 1, - 1, null, null, - 1, null, null, null, - 1, - 1, null, - 1, - 0, + null, null, null, null, 1, - 0, + 4, null, + 1, + 24, null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/config/environment.rb": { + "lines": [ null, null, + 1, + 1, + 1, + 1, + 1, null, 1, - 0, null, + 1, + 1, + null, + null, + 1, null, - 0, null, null, + 1, + 1, null, + 1, + 0, null, null, null, @@ -110,219 +115,163 @@ null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/contributions/entities/contributor.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/contributions/values/appraisal.rb": { "lines": [ null, null, 1, 1, null, - 1, + null, 1, null, + null, + null, + null, 1, 1, null, + null, 1, 1, null, null, 1, - 7555, - null, + 1, null, null, 1, null, null, 1, - 6940, null, null, + 1, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/contributions/entities/file_contributions.rb": { - "lines": [ null, null, 1, - 1, null, - 1, - 1, null, 1, 1, null, 1, - null, - 1, - 190, - 190, + 12, null, null, 1, - 711, + 14, null, null, 1, - 199, + 2, null, null, 1, - 711, - null, - 711, - null, - 5360, + 4, null, null, null, 1, - 66, + 7, null, null, - 1, null, - 1, - 1, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/contributions/lib/contributions_calculator.rb": { - "lines": [ null, null, - 1, - 1, null, - 1, - 1, - 199, null, null, 1, - 0, + 14, null, null, - 1, - 0, null, null, - 1, - 0, null, null, - 1, - 0, null, - 0, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/contributions/values/code_language.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/projects/entities/project.rb": { "lines": [ - null, null, null, 1, 1, + null, 1, + null, 1, 1, null, - null, 1, - null, 1, - 8, - null, null, 1, - 6496, - null, null, 1, - 0, - null, - null, 1, 1, - null, 1, - 9, - null, - null, 1, - 5360, - null, - null, 1, - 0, - null, - null, 1, - 11870, + 1, null, + 1, + 25, null, null, 1, - 1, + 11, null, - 1, - 1, - 8, - 8, - 8, null, - 8, + 1, null, + 17, null, null, - 1, - 1, null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/projects/entities/member.rb": { + "lines": [ null, null, 1, 1, null, 1, - null, - 1, 1, - 1, - null, null, 1, 1, null, 1, - null, 1, 1, 1, null, - null, - 1, 1, + 65, null, - 1, null, - 1, - 1, - 1, null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/cache/remote_cache.rb": { + "lines": [ null, - 1, - 1, null, 1, null, @@ -330,146 +279,102 @@ 1, null, null, - 1, - 1, null, - 1, + null, null, 1, 1, + 48, null, null, - 1, - 1, null, - 1, null, - 1, - 1, null, null, 1, - 1, + 8, null, - 1, null, - 1, - 1, null, null, - 1, - 1, null, 1, + 11, null, - 1, - 1, null, null, - 1, - 1, null, - 1, null, 1, - 1, + 3, + null, null, 1, + 61, null, null, 1, + 70, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/contributions/entities/folder_contributions.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/database/orm/member_orm.rb": { "lines": [ null, null, 1, - 1, null, 1, 1, null, 1, - null, 1, - 48, null, - 48, - 48, - 48, - 48, null, null, 1, - 17, null, null, - 1, - 17, null, null, 1, - 0, - null, null, 1, - 17, - null, + 64, null, - 1, - 17, null, null, - 1, - 58, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/database/orm/project_orm.rb": { + "lines": [ null, null, 1, - 17, - null, null, 1, - null, 1, - 584, - null, null, 1, - 48, - null, - null, 1, - 48, - 387, - 190, null, null, 1, - 48, null, - 197, - 197, null, null, null, 1, - 48, - 43, - null, - null, - 91, - null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/contributions/entities/line_contribution.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/database/repositories/for.rb": { "lines": [ null, null, @@ -477,32 +382,27 @@ 1, null, 1, - null, - 1, 1, null, 1, - 1, - 1, null, 1, null, + null, + null, 1, - 5360, - 5360, - 5360, - 5360, + 30, null, null, 1, - 11870, + 16, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/contributions/lib/types.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/database/repositories/members.rb": { "lines": [ null, null, @@ -511,164 +411,123 @@ null, 1, 1, - 139, - null, - null, - null, - null, - 1, - 1, - 1845, - null, - null, - null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/contributions/values/contributors.rb": { - "lines": [ - 1, - 1, + 0, null, - 1, - 1, null, 1, - 356, - null, - null, - null, + 0, null, null, 1, + 120, null, - 356, - 356, + 120, null, - 356, null, - 1091, - 1091, null, - 1091, null, - 0, - 0, - 0, - 0, - 0, - 0, - null, - 1091, - null, - 86, - 86, - 86, null, null, - 1005, - 1005, - 1005, null, + 1, + 30, + 90, null, null, - 356, null, + 1, + 64, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/contributions/values/credit_share.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/database/repositories/projects.rb": { "lines": [ null, null, 1, - 1, null, 1, + 1, null, 1, 1, + 0, + null, null, 1, - 546, - 546, null, null, null, - 1, - 0, null, + 29, null, - 1, - 0, null, null, - 1, - 0, + null, + 29, null, null, 1, - 0, + 5, + 4, + 4, null, null, - 1, null, 1, - 0, + 16, null, null, 1, - 199, + 0, + 0, null, null, 1, - 6365, - 6365, - 6365, - 6365, + 16, + 16, null, null, 1, - 356, + 16, null, + 16, + 16, null, null, - 356, - 446, + 1, + 61, null, + 30, null, - 356, - 1005, - 2096, - 1005, null, null, null, - 1, - 7, - 3, null, null, - 1, - 3, null, null, 1, - null, 1, - 13305, + 16, null, null, + 1, + 16, null, null, 1, - 0, - 0, + 16, + null, + 16, + 16, + null, + 16, + 48, null, - 0, null, null, null, @@ -677,7 +536,7 @@ null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/contributions/values/file_path.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/github/gateways/github_api.rb": { "lines": [ null, null, @@ -687,121 +546,135 @@ 1, null, 1, - null, 1, null, 1, + 55, + null, null, 1, - 760, - 760, + 30, null, null, - 1, - 12887, null, null, 1, - 6451, + 25, null, - 6436, null, null, 1, - 380, + 1, + 30, + 30, null, null, 1, + 30, null, - 197, null, - 197, - 197, null, null, 1, + 1, + 55, + null, null, 1, - 760, - 760, - 760, + 55, null, null, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/projects/entities/member.rb": { - "lines": [ + 54, + 54, null, null, - 1, - 1, null, - 1, - 1, null, - 1, - 1, null, 1, 1, 1, + null, + null, + 1, + null, + null, + null, 1, + 54, + null, null, 1, - 57, + 4, + null, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/domain/projects/entities/project.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/github/mappers/member_mapper.rb": { "lines": [ null, null, 1, - 1, null, 1, null, 1, 1, + 25, + 25, + 25, null, - 1, - 1, null, 1, + 25, + 75, + null, + null, null, 1, + 100, + null, + null, + null, 1, 1, + 100, + null, + null, 1, - 1, - 1, - 1, + 100, + null, + null, + null, + null, + null, + null, + null, 1, null, 1, - 7, + 100, null, null, 1, - 10, + 100, null, null, 1, + 100, null, - 15, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/cache/local_cache.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/github/mappers/project_mapper.rb": { "lines": [ null, null, @@ -812,84 +685,76 @@ null, 1, 1, - 5, - 5, + 30, + 30, + 30, null, null, 1, - 13, + 30, + 25, null, null, 1, - 2, + 25, null, null, - 1, null, 1, - 5, - null, + 1, + 25, + 25, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/cache/redis_cache.rb": { - "lines": [ null, null, 1, + 25, null, - 1, - 1, null, - 1, - 1, - 0, null, null, - 1, - 0, null, null, - 1, - 0, null, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/database/orm/member_orm.rb": { - "lines": [ null, null, 1, + 25, null, - 1, - 1, null, 1, - 1, + 25, + null, null, + 1, + 25, null, null, 1, + 25, null, null, + 1, + 25, null, null, 1, + 25, + null, null, 1, - 56, + 25, + null, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/database/orm/project_orm.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/messaging/queue.rb": { "lines": [ null, null, @@ -898,253 +763,187 @@ 1, 1, null, + null, 1, 1, null, - null, 1, + 3, + 3, null, null, null, null, - 1, + 3, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/database/repositories/for.rb": { - "lines": [ null, null, - 1, - 1, null, - 1, - 1, null, 1, + 3, null, - 1, null, null, null, - 1, - 28, null, null, 1, - 14, + 0, + 0, + 0, + null, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/database/repositories/members.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/appraisal_representer.rb": { "lines": [ null, null, 1, 1, + 1, null, 1, 1, - 0, - null, null, 1, - 0, - null, - null, 1, - 104, - null, - 104, - null, null, null, null, + 1, + 1, null, + 1, + 1, + 1, null, null, 1, - 26, - 78, + 6, null, null, + 7, + 7, + null, null, 1, - 56, + 6, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/database/repositories/projects.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/project_representer.rb": { "lines": [ null, null, 1, - null, 1, 1, null, 1, - 1, - 0, null, null, 1, - null, - null, - null, - null, - 27, - null, - null, - null, - null, - 27, - null, - null, 1, - 5, - 4, - 4, - null, - null, null, 1, - 14, - null, - null, 1, - 0, - 0, + 1, + 1, null, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1, null, 1, 14, - 14, null, null, 1, - 14, null, - 14, + 1, 14, null, null, 1, - 55, - null, - 26, - null, - null, - null, + 14, null, null, null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/member_representer.rb": { + "lines": [ null, null, 1, 1, - 14, - null, null, 1, - 14, - null, - null, 1, - 14, null, - 14, - 14, - null, - 14, - 42, null, null, null, + 1, + 1, null, + 1, + 1, + 1, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/gateway/git_command.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/folder_contributions_representer.rb": { "lines": [ null, null, 1, 1, + 1, null, 1, 1, + 1, + 1, null, 1, - 194, - 194, - 194, - 194, + 1, null, + 1, + 1, null, 1, - 3, - 3, - 3, - null, - null, 1, - 191, - 191, - 191, - null, - null, 1, - 191, - 191, - null, - null, 1, - 3, - 3, - null, - null, 1, - 3, - 3, - null, - null, 1, - 388, - null, - null, 1, - 194, - null, - null, - null, - null, - null, 1, - 190, - null, - null, 1, - 2, - 10, - null, - null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/mappers/blame_contributor.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/contributor_representer.rb": { "lines": [ null, null, @@ -1153,236 +952,150 @@ null, 1, 1, - 5360, - 5360, - null, null, 1, - 5360, - null, - null, - null, - null, - null, - null, 1, null, 1, - 5360, - null, + 1, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/mappers/contributions_mapper.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/credit_share_representer.rb": { "lines": [ null, null, 1, 1, - null, - 1, 1, - 6, - null, null, 1, - 6, - null, - 5, - null, - null, - null, - null, null, 1, 1, - 5, - 190, - null, - null, - null, null, 1, 1, - 190, - 190, - 190, null, + 1, + 1, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/mappers/file_contributions_mapper.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/file_contributions_representer.rb": { "lines": [ null, null, 1, - null, 1, 1, null, 1, 1, - 190, - null, - null, 1, - 190, - null, - null, - null, - null, - null, 1, null, 1, - 380, - null, - null, 1, - 5360, - null, null, 1, - 190, - null, - null, 1, - 190, - 5360, null, + 1, + 1, + 1, + 1, + 1, null, null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/file_path_representer.rb": { + "lines": [ null, null, 1, 1, - 5360, - 5360, - 5360, null, - null, - 1, - 1, 1, + null, 1, 1, null, 1, - 5360, - null, - null, - null, 1, null, 1, - 5360, - null, + 1, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/mappers/folder_contributions_mapper.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/line_contribution_representer.rb": { "lines": [ null, null, 1, - null, 1, 1, null, 1, - 1, - null, null, 1, - 5, - 5, - null, - null, 1, - 5, - null, - null, - null, - null, null, 1, - 5, - 190, - null, + 1, null, + 1, + 1, + 1, + 1, + 1, + 1, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/mappers/porcelain_parser.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/appraisal_request_representer.rb": { "lines": [ null, null, 1, 1, - null, - 1, - 1, 1, null, 1, - 190, - 5360, - null, - null, 1, - 190, - 190, - null, - 0, - 0, - null, null, null, 1, - 5360, - null, - 5360, - null, - null, - null, - 5360, - 56896, - 56896, - null, - null, - 5360, - null, - null, 1, - 5360, - 5360, - 5360, - null, null, 1, - 56896, - null, + 1, + 1, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/repositories/blame_reporter.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/clone_request_representer.rb": { "lines": [ null, null, 1, - null, 1, + 1, + null, null, 1, 1, @@ -1391,258 +1104,1419 @@ 1, null, 1, - 6, - 6, - 6, - null, - null, 1, - 6, null, - 310, null, - 10, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/http_response_representer.rb": { + "lines": [ null, null, 1, - 0, - 0, - 0, + 1, null, - 0, + 1, + 1, null, null, null, - 1, - 0, null, null, 1, - 190, - null, + 1, null, 1, + 1, null, 1, - 6, null, - 6, null, null, - 1, - 1, null, null, null, - 1, - 0, null, null, null, - 1, - 5, - 190, - 190, - 190, null, null, null, null, + 1, + 11, + null, + null, + null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/repositories/repo_file.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/openstruct_with_links.rb": { "lines": [ null, null, 1, - 1, null, 1, 1, null, 1, - 190, - null, - null, 1, - 190, - null, - null, - null, - null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/repositories/git_repo.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/projects_representer.rb": { "lines": [ null, null, 1, - null, - 1, - 1, 1, + null, 1, 1, null, - null, 1, - 11, - 11, - 11, - null, - null, 1, - 6, - null, null, 1, - 0, - null, - null, 1, - 19, - null, null, 1, - 2, - 2, null, - 12, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/repositories/local_repo.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/responses/api_result.rb": { "lines": [ null, null, 1, - null, - 1, 1, 1, null, - 1, - null, null, null, 1, - 1, - 1, null, - 1, null, - 1, - 11, - 11, null, null, 1, - 12, - 2, null, null, 1, - 5, - null, - 5, - 5, - 460, + 1, + 30, null, + 29, null, null, null, - 1, - 13, - 26, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/responses/clone_request.rb": { + "lines": [ null, null, 1, - 37, - null, + 1, null, 1, - 0, + null, null, null, 1, - 0, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/responses/projects_list.rb": { + "lines": [ null, null, 1, - null, 1, - 18, - null, null, + 1, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/repositories/remote_repo.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/controllers/app.rb": { "lines": [ null, null, 1, + 1, + null, + 1, null, 1, 1, + 1, null, null, null, null, + 1, + 14, null, null, - 1, + 14, 1, null, 1, - 11, null, null, - 1, - 11, + null, + 1, + 1, + null, + null, + 13, + 13, + 13, + null, + 10, + null, + null, + 8, + null, + 8, + null, + null, + null, + 8, + null, + null, + null, + null, + null, + 8, + 5, + 5, + null, + null, + null, + 3, + 3, + 3, + 3, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + 2, + 2, + null, + null, + null, + 2, + 1, + 1, + null, + null, + 1, + 1, + 1, + null, + null, + null, + 3, + null, + 3, + 3, + 3, + null, + 3, + 1, + 1, + null, + null, + 2, + 2, + 2, + null, + null, + null, + null, + null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/controllers/helpers.rb": { + "lines": [ + null, + null, + 1, + 1, + null, + 1, + 1, + 0, + 0, + 0, + 0, + null, + null, + 1, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/requests/project_list.rb": { + "lines": [ + null, + null, + 1, + 1, + 1, + null, + 1, + 1, + null, + 1, + 1, + null, + 1, + 6, + null, + null, + null, + 1, + 6, + null, + null, + null, + 1, + null, + null, + null, + null, + null, + null, + null, + null, + 1, + 6, + null, + null, + null, + null, + 1, + 5, + null, + null, + null, + 1, + 3, + null, + null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/requests/project_request_path.rb": { + "lines": [ + null, + null, + 1, + 1, + null, + 1, + 1, + 8, + 8, + 8, + 8, + null, + null, + 1, + null, + 1, + 9, + null, + null, + 1, + 0, + null, + null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/services/add_project.rb": { + "lines": [ + null, + null, + 1, + null, + 1, + 1, + null, + 1, + 1, + null, + 1, + 1, + null, + 1, + null, + 1, + 1, + null, + null, + 1, + 12, + 1, + null, + 11, + null, + 9, + null, + 3, + null, + null, + 1, + null, + 9, + 8, + null, + 1, + null, + 9, + null, + 0, + 0, + null, + null, + null, + null, + 1, + 11, + null, + null, + null, + 3, + null, + null, + 1, + 12, + null, + null, + null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/services/fetch_or_request_appraisal.rb": { + "lines": [ + null, + null, + 1, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + 1, + 1, + 1, + 1, + null, + 1, + null, + 1, + 1, + 1, + 1, + 1, + null, + null, + 1, + 12, + null, + null, + null, + 12, + 10, + null, + 2, + null, + null, + 0, + null, + null, + 1, + 10, + 2, + null, + 8, + null, + null, + null, + 1, + 8, + 8, + null, + 8, + 8, + null, + null, + null, + 4, + 4, + 4, + null, + null, + 0, + 0, + null, + null, + 1, + null, + 8, + null, + null, + 4, + null, + null, + 4, + null, + null, + null, + null, + 0, + 0, + null, + null, + null, + null, + 1, + 8, + 8, + null, + null, + 1, + 4, + null, + null, + null, + 4, + null, + null, + null, + 1, + 0, + null, + null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/services/list_projects.rb": { + "lines": [ + null, + null, + 1, + null, + 1, + 1, + null, + 1, + 1, + null, + 1, + 1, + null, + 1, + null, + 1, + null, + null, + 1, + 6, + 6, + 5, + null, + 1, + null, + null, + null, + 1, + 5, + 5, + 5, + 5, + null, + 0, + null, + null, + null, + null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/entities/contributor.rb": { + "lines": [ + null, + null, + 1, + 1, + null, + 1, + 1, + null, + 1, + 1, + null, + 1, + 1, + null, + null, + 1, + 2473, + null, + null, + null, + 1, + null, + null, + 1, + 2264, + null, + null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/entities/file_contributions.rb": { + "lines": [ + null, + null, + 1, + 1, + null, + 1, + 1, + null, + 1, + 1, + null, + 1, + null, + 1, + 63, + 63, + null, + null, + 1, + 125, + null, + null, + 1, + 0, + null, + null, + 1, + 125, + null, + 125, + null, + 1724, + null, + null, + null, + 1, + 0, + null, + null, + 1, + null, + 1, + 1, + null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/lib/contributions_calculator.rb": { + "lines": [ + null, + null, + 1, + 1, + null, + 1, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + 0, + null, + null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/values/code_language.rb": { + "lines": [ + null, + null, + null, + 1, + 1, + 1, + 1, + 1, + null, + null, + 1, + null, + 1, + 8, + null, + null, + 1, + 2019, + null, + null, + 1, + 0, + null, + null, + 1, + 1, + null, + 1, + 9, + null, + null, + 1, + 1724, + null, + null, + 1, + 0, + null, + null, + 1, + 1724, + null, + null, + null, + 1, + 1, + null, + 1, + 1, + 8, + 8, + 8, + null, + 8, + null, + null, + null, + 1, + 1, + null, + null, + null, + 1, + 1, + null, + 1, + null, + 1, + 1, + 1, + null, + null, + 1, + 1, + null, + 1, + null, + 1, + 1, + 1, + null, + null, + 1, + 1, + null, + 1, + null, + 1, + 1, + 1, + null, + null, + 1, + 1, + null, + 1, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + 1, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + 1, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + 1, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + 1, + null, + 1, + 1, + null, + null, + 1, + 1, + null, + 1, + null, + 1, + 1, + null, + 1, + null, + null, + 1, + null, + null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/entities/folder_contributions.rb": { + "lines": [ + null, + null, + 1, + 1, + null, + 1, + 1, + null, + 1, + null, + 1, + 23, + null, + 23, + 23, + 23, + 23, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 12, + null, + null, + 1, + 0, + null, + null, + 1, + null, + 1, + 193, + null, + null, + 1, + 23, + null, + null, + 1, + 23, + 128, + 63, + null, + null, + 1, + 23, + null, + 65, + 65, + null, + null, + null, + 1, + 23, + 14, + null, + null, + 37, + null, + null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/entities/line_contribution.rb": { + "lines": [ + null, + null, + 1, + 1, + null, + 1, + null, + 1, + 1, + null, + 1, + 1, + 1, + null, + 1, + null, + 1, + 1724, + 1724, + 1724, + 1724, + null, + null, + 1, + 1724, + null, + null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/lib/types.rb": { + "lines": [ + null, + null, + 1, + 1, + null, + 1, + 1, + 60, + null, + null, + null, + null, + 1, + 1, + 623, + null, + null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/values/contributors.rb": { + "lines": [ + 1, + 1, + null, + 1, + 1, + null, + 1, + 120, + null, + null, + null, + null, + null, + 1, + null, + 120, + 120, + null, + 120, + null, + 371, + 371, + null, + 371, + null, + 0, + 0, + 0, + 0, + 0, + 0, + null, + 371, + null, + 29, + 29, + 29, + null, + null, + 342, + 342, + 342, + null, + null, + null, + 120, + null, + null, + null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/values/credit_share.rb": { + "lines": [ + null, + null, + 1, + 1, + null, + 1, + null, + 1, + 1, + null, + 1, + 183, + 183, + null, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + null, + 1, + 0, + null, + null, + 1, + 0, + null, + null, + 1, + 2066, + 2066, + 2066, + 2066, + null, + null, + 1, + 120, + null, + null, + null, + 120, + 155, + null, + null, + 120, + 342, + 713, + 342, + null, + null, + null, + 1, + 7, + 3, + null, + null, + 1, + 3, + null, + null, + 1, + null, + 1, + 4329, + null, + null, + null, + null, + 1, + 0, + 0, + null, + 0, + null, + null, + null, + null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/domain/contributions/values/file_path.rb": { + "lines": [ + null, + null, + 1, + null, + 1, + 1, + null, + 1, + null, + 1, + null, + 1, + null, + 1, + 278, + 278, + null, + null, + 1, + 3996, + null, + null, + 1, + 2001, + null, + 1995, + null, + null, + 1, + 152, + null, + null, + 1, + null, + 65, + null, + 65, + 65, + null, + null, + 1, + null, + 1, + 278, + 278, + 278, + null, + null, + null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/infrastructure/git/gateway/git_command.rb": { + "lines": [ + null, + null, + 1, + 1, + null, + 1, + 1, + null, + 1, + 66, + 66, + 66, + 66, + null, + null, + 1, + 2, + 2, + 2, + null, + null, + 1, + 64, + 64, + 64, + null, + null, + 1, + 64, + 64, + null, + null, + 1, + 2, + 2, + null, + null, + 1, + 2, + 2, + null, + null, + 1, + 132, + null, + null, + 1, + 66, + null, + null, + null, + null, + null, + 1, + 63, + null, + null, + 1, + 1, + 5, + null, + null, + null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/infrastructure/git/mappers/blame_contributor.rb": { + "lines": [ + null, + null, + 1, + 1, + null, + 1, + 1, + 1724, + 1724, + null, + null, + 1, + 1724, + null, + null, + null, + null, + null, + null, + 1, + null, + 1, + 1724, + null, + null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/infrastructure/git/mappers/contributions_mapper.rb": { + "lines": [ + null, + null, + 1, + 1, + null, + 1, + 1, + 2, null, null, 1, 2, null, + 2, + null, + null, + null, + null, + null, + 1, + 1, + 2, + 63, + null, null, null, null, + 1, + 1, + 63, + 63, + 63, + null, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/git/repositories/repo_store.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/infrastructure/git/mappers/file_contributions_mapper.rb": { "lines": [ null, null, 1, + null, + 1, 1, null, 1, 1, - 11, - 16, + 63, null, null, 1, - 16, + 63, + null, + null, + null, + null, + null, + 1, + null, + 1, + 126, + null, + null, + 1, + 1724, + null, + null, + 1, + 63, + null, + null, + 1, + 63, + 1724, + null, + null, + null, + null, + null, + 1, + 1, + 1724, + 1724, + 1724, + null, + null, + 1, + 1, + 1, + 1, + 1, + null, + 1, + 1724, + null, + null, + null, + 1, + null, + 1, + 1724, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/github/gateways/github_api.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/infrastructure/git/mappers/folder_contributions_mapper.rb": { "lines": [ null, null, @@ -1654,133 +2528,292 @@ 1, 1, null, + null, 1, - 51, + 2, + 2, null, null, 1, - 28, + 2, + null, null, null, null, null, 1, - 23, + 2, + 63, + null, + null, + null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/infrastructure/git/mappers/porcelain_parser.rb": { + "lines": [ + null, + null, + 1, + 1, + null, + 1, + 1, + 1, + null, + 1, + 63, + 1724, + null, + null, + 1, + 63, + 63, + null, + 0, + 0, + null, + null, + null, + 1, + 1724, + null, + 1724, + null, + null, + null, + 1724, + 18292, + 18292, + null, + null, + 1724, + null, + null, + 1, + 1724, + 1724, + 1724, + null, + null, + 1, + 18292, + null, + null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/infrastructure/git/repositories/blame_reporter.rb": { + "lines": [ + null, + null, + 1, + null, + 1, + null, + 1, + 1, + null, + 1, + 1, + null, + 1, + 2, + 2, + 2, null, null, + 1, + 2, + null, + 124, + null, + 4, + null, + null, + 1, + 0, + 0, + 0, + null, + 0, + null, + null, + null, + 1, + 0, + null, + null, + 1, + 63, + null, + null, + 1, null, 1, + 2, + null, + 2, + null, + null, + 1, + 0, + null, + null, + null, 1, - 28, - 28, + 0, + null, null, null, 1, - 28, + 2, + 63, + 63, + 63, + null, + null, + null, null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/infrastructure/git/repositories/repo_file.rb": { + "lines": [ null, null, + 1, + 1, null, 1, 1, - 51, + null, + 1, + 63, null, null, 1, - 51, + 63, null, null, null, null, - 50, - 50, null, null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/infrastructure/git/repositories/git_repo.rb": { + "lines": [ + null, + null, + 1, + null, + 1, + 1, + 1, + 1, + 1, + null, null, + 1, + 3, + 3, + 3, null, null, 1, - 1, - 1, + 2, null, null, 1, - null, + 0, null, null, 1, - 50, + 6, null, null, 1, - 4, - null, + 1, + 1, null, + 6, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/github/mappers/member_mapper.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/infrastructure/git/repositories/local_repo.rb": { "lines": [ null, null, 1, null, 1, - null, 1, 1, - 23, - 23, - 23, - null, null, 1, - 23, - 69, null, null, null, 1, - 92, + 1, + 1, + null, + 1, null, + 1, + 3, + 3, null, null, 1, + 6, 1, - 92, null, null, 1, - 92, + 2, null, + 2, + 2, + 184, null, null, null, null, + 1, + 5, + 10, null, null, 1, + 13, + null, null, 1, - 92, + 0, null, null, 1, - 92, + 0, null, null, 1, - 92, null, + 1, + 7, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/github/mappers/project_mapper.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/infrastructure/git/repositories/remote_repo.rb": { "lines": [ null, null, @@ -1789,197 +2822,135 @@ 1, 1, null, - 1, - 1, - 28, - 28, - 28, - null, null, - 1, - 28, - 23, null, null, - 1, - 23, - null, null, null, 1, 1, - 23, - 23, - null, - null, - null, null, 1, - 23, - null, - null, + 3, null, null, + 1, + 3, null, null, + 1, + 1, null, null, null, null, null, - 1, - 23, null, null, - 1, - 23, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/infrastructure/git/repositories/repo_store.rb": { + "lines": [ null, null, 1, - 23, - null, - null, 1, - 23, - null, null, 1, - 23, - null, - null, 1, - 23, + 11, + 15, null, null, 1, - 23, - null, + 15, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/infrastructure/messaging/queue.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/infrastructure/messaging/progress_publisher.rb": { "lines": [ null, null, 1, null, 1, - 1, null, - null, - 1, 1, - null, 1, - 3, - 3, - null, + 0, + 0, null, null, + 1, + 0, + 0, + 0, null, - 3, null, null, null, + 0, null, + 0, null, null, 1, - 3, - null, - null, - null, - null, - null, null, 1, 0, - 0, - 0, - null, null, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/clone_request_representer.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/presentation/values/progress_monitor.rb": { "lines": [ null, null, 1, - 1, - 1, null, null, 1, 1, null, - 1, - 1, null, - 1, - 1, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/project_representer.rb": { - "lines": [ null, null, - 1, - 1, - 1, null, - 1, null, null, 1, - 1, + 0, null, - 1, - 1, - 1, - 1, null, 1, - 1, - 1, - 1, - 1, - 1, - 1, - 1, + 0, + null, null, 1, - 7, + 0, null, null, 1, + 0, + null, null, 1, - 7, + 0, null, null, - 1, - 7, null, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/member_representer.rb": { - "lines": [ null, null, - 1, - 1, null, 1, 1, @@ -1987,113 +2958,107 @@ null, null, null, - 1, - 1, null, - 1, - 1, - 1, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/contributor_representer.rb": { - "lines": [ null, null, - 1, - 1, null, - 1, - 1, null, 1, - 1, + 0, + null, null, 1, - 1, + 0, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/credit_share_representer.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/helpers/vcr_helper.rb": { "lines": [ null, null, 1, 1, - 1, null, - 1, null, 1, 1, - null, - 1, 1, null, 1, + 7, + 7, + 7, + 7, + 7, + 7, + null, + null, + null, + null, 1, + 32, + 2462, + 2462, null, null, + 32, + null, + null, + null, + null, + null, + null, + null, + 1, + 32, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/file_contributions_representer.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/helpers/database_helper.rb": { "lines": [ null, null, - 1, - 1, - 1, - null, - 1, - 1, - 1, - 1, - null, - 1, - 1, null, 1, - 1, null, 1, - 1, - 1, - 1, - 1, null, + 24, + 24, + 24, + 24, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/file_path_representer.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/helpers/cache_helper.rb": { "lines": [ null, null, - 1, - 1, null, - 1, null, - 1, - 1, null, - 1, - 1, null, 1, + null, 1, + 40, + null, null, null, + 1, + 58, + 58, + null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/line_contribution_representer.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/integration/layers/domain_contributions_spec.rb": { "lines": [ null, null, @@ -2102,164 +3067,157 @@ 1, null, 1, - null, - 1, 1, null, 1, - 1, + 2, + 2, null, - 1, - 1, - 1, - 1, - 1, - 1, + 2, + null, + null, + null, + 2, + null, + null, + 2, + 2, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/folder_contributions_representer.rb": { - "lines": [ + 1, + 2, null, null, 1, 1, 1, + 1, null, 1, 1, 1, + null, + null, 1, null, + null, 1, + null, + null, 1, null, + null, + null, 1, 1, null, 1, + null, 1, + null, 1, 1, + null, 1, 1, - 1, + null, 1, 1, null, - null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/http_response_representer.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/integration/layers/gateway_database_spec.rb": { "lines": [ null, null, 1, 1, + 1, null, 1, 1, null, - null, - null, + 1, + 1, null, null, 1, 1, null, + null, 1, 1, - null, 1, null, null, + 1, + 1, null, null, null, + 1, null, + 1, + 1, + 1, + 1, + 1, + 1, null, + 1, + 3, + 6, null, null, + 3, null, null, null, null, - 1, - 14, - null, - null, - null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/openstruct_with_links.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/integration/layers/github_spec.rb": { "lines": [ null, null, 1, - null, - 1, 1, null, 1, 1, null, - null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/project_folder_contributions_representer.rb": { - "lines": [ - null, - null, - 1, - 1, 1, + 8, null, - 1, - 1, null, 1, - 1, + 8, null, - 1, - 1, null, 1, 1, + 4, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/representers/projects_representer.rb": { - "lines": [ null, null, 1, 1, - null, - 1, 1, null, + null, 1, 1, null, + null, 1, 1, - null, 1, null, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/responses/api_result.rb": { - "lines": [ null, null, 1, @@ -2268,81 +3226,65 @@ null, null, null, - 1, - null, - null, - null, null, - 1, null, null, 1, 1, - 32, + 4, null, - 31, null, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/responses/clone_request.rb": { - "lines": [ + 1, + 1, null, null, 1, 1, - 1, - null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/responses/project_folder_contributions.rb": { - "lines": [ null, null, 1, 1, - null, 1, - null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/presentation/responses/projects_list.rb": { - "lines": [ null, null, 1, 1, + 1, null, 1, + 4, + 1, + null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/controllers/app.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/integration/services/add_project_spec.rb": { "lines": [ null, null, 1, 1, - null, 1, null, 1, 1, + null, 1, + 3, null, null, + 1, + 3, null, null, 1, - 14, + 1, + 3, null, null, - 14, 1, null, 1, @@ -2350,50 +3292,47 @@ null, null, 1, - 1, null, null, - 13, - 13, - 13, null, - 10, - 8, - 0, null, + 1, null, - 8, null, - 8, + 1, null, + 1, + 1, + 1, + 1, + 1, + 1, null, + 1, + 3, + 6, null, - 8, null, + 3, null, null, null, null, - 8, - 6, - 6, + 1, null, + 1, null, - 2, - 2, null, - 2, null, null, + 1, null, null, null, - 2, - 2, null, + 1, null, null, - 2, 1, 1, null, @@ -2401,82 +3340,80 @@ 1, 1, 1, + 1, + 1, + 1, null, + 1, + 3, + 6, null, null, 3, null, - 3, - 3, - 3, null, - 3, - 1, - 1, null, null, - 2, - 2, - 2, + 1, + null, + 1, null, null, null, null, + 1, + 1, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/controllers/helpers.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/integration/services/fetch_or_request_appraisal_spec.rb": { "lines": [ null, null, 1, 1, - null, 1, 1, - 0, - 0, - 0, - 0, - null, null, 1, null, 1, - 0, - null, + 1, null, 1, - 0, + 4, + 4, + 4, + 4, null, + 4, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/requests/project_list.rb": { - "lines": [ + 1, + 4, + 4, null, null, 1, 1, - 1, null, 1, + null, + null, 1, null, + null, + 1, 1, 1, null, + null, 1, - 6, null, null, null, - 1, - 6, null, null, null, @@ -2487,194 +3424,177 @@ null, null, null, + 1, + 1, + 1, + null, null, + 1, null, 1, - 6, null, null, + 1, null, null, 1, - 5, null, null, null, - 1, - 3, null, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/requests/project_request_path.rb": { - "lines": [ null, + 1, + 1, null, 1, 1, null, + null, + null, + null, + null, + null, + 1, 1, 1, - 8, - 8, - 8, - 8, null, null, 1, null, + null, 1, - 3, + null, null, null, 1, - 0, null, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/services/add_project.rb": { - "lines": [ + null, null, null, 1, null, + null, + null, + null, + null, + null, 1, 1, null, - 1, - 1, null, 1, - 1, null, 1, null, - 1, - 1, null, null, - 1, - 12, + null, 1, null, - 11, null, - 9, null, - 3, + null, + null, + null, + null, null, null, 1, null, - 9, - 8, null, 1, null, - 9, null, - 0, - 0, null, null, null, null, 1, - 11, - null, null, null, - 3, null, null, - 1, - 12, null, null, + 1, + 1, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/services/appraise_project.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/integration/services/list_projects_spec.rb": { "lines": [ null, null, 1, - null, 1, 1, null, 1, - 1, null, 1, 1, - 1, - 1, null, 1, + 3, null, null, 1, + 3, + null, + null, 1, 1, - 1, - 1, + 3, + null, + null, 1, null, + 1, null, null, 1, - 10, null, null, null, - 10, - 8, + 1, null, - 2, null, + 1, null, - 0, null, null, 1, - 8, 1, - null, - 7, - 7, + 1, null, null, + 1, null, 1, - 7, null, - 3, null, + 1, null, - 3, null, null, + 1, + 1, null, null, - 0, - 0, null, null, 1, - 4, - null, + 1, + 1, null, - 3, - 3, null, + 1, null, 1, null, @@ -2682,26 +3602,19 @@ null, null, 1, - 0, null, null, null, null, 1, - 0, - null, - null, 1, - 3, - 3, - null, - null, + 1, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/app/application/services/list_projects.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/unit/appraisal_spec.rb": { "lines": [ null, null, @@ -2710,181 +3623,176 @@ 1, 1, null, - 1, - 1, + 15, null, - 1, - 1, null, - 1, null, - 1, null, null, - 1, - 6, - 6, - 5, null, - 1, + 15, null, null, null, - 1, - 5, - 5, - 5, - 5, null, - 0, null, null, null, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/helpers/vcr_helper.rb": { - "lines": [ null, null, 1, 1, null, null, - 1, - 1, - 1, - null, - 1, - 7, - 7, - 7, - 7, - 7, 7, null, null, null, null, - 1, - 30, - 2005, - 2005, - null, + 7, null, - 30, null, null, null, null, null, + 1, + 1, + 1, null, null, 1, - 30, + 1, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/helpers/database_helper.rb": { - "lines": [ null, + 1, + 1, null, null, 1, + 1, null, + null, + 1, + 1, 1, null, - 22, - 22, - 22, - 22, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/integration/layers/domain_contributions_spec.rb": { - "lines": [ + 1, + 1, null, null, 1, 1, 1, null, + null, + null, 1, 1, + 7, null, - 1, - 2, - 2, null, - 2, null, null, null, - 2, null, null, - 2, - 2, + 1, + 1, + 1, null, null, 1, - 2, + 1, null, null, 1, 1, + null, + null, 1, 1, null, + null, 1, 1, 1, null, null, 1, - null, - null, 1, null, null, 1, + 1, + 1, null, null, null, 1, 1, - null, 1, null, - 1, + null, + null, + null, + null, + null, null, 1, 1, null, + null, + null, + null, 1, 1, + 6, + null, + null, + null, + null, + null, + null, + 6, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, + null, null, 1, 1, + 6, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/integration/layers/gateway_database_spec.rb": { - "lines": [ + null, + null, + null, + null, + null, + 6, + 6, null, null, 1, 1, - 1, + null, null, 1, 1, null, + null, 1, 1, null, @@ -2895,37 +3803,47 @@ null, 1, 1, - 1, null, null, 1, 1, null, + null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/unit/gateway_git_spec.rb": { + "lines": [ null, null, 1, null, 1, 1, + null, 1, 1, 1, - 1, + null, + null, + null, + null, null, 1, - 3, - 6, null, null, - 3, + 1, + 1, + null, null, null, null, + 1, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/integration/layers/github_spec.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/unit/remote_cache_spec.rb": { "lines": [ null, null, @@ -2934,30 +3852,28 @@ null, 1, 1, - null, - 1, - 8, + 7, + 7, null, null, 1, - 8, + 7, null, null, 1, 1, - 4, - null, + 1, null, + 1, null, null, 1, 1, - 1, null, null, 1, 1, - null, + 1, null, 1, 1, @@ -2965,184 +3881,153 @@ null, null, null, - null, - null, 1, 1, 1, null, - null, - null, - null, + 1, null, null, 1, 1, - 4, - null, null, null, null, 1, 1, - null, - null, 1, 1, null, + 1, null, 1, 1, 1, null, null, + null, + 1, 1, 1, 1, null, 1, - 4, + null, + 1, 1, null, null, null ] }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/integration/services/add_project_spec.rb": { + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/unit/result_spec.rb": { "lines": [ null, null, 1, - 1, - 1, null, 1, 1, - null, 1, - 3, - null, null, 1, - 3, + 1, null, null, 1, 1, - 3, - null, null, 1, - null, 1, null, null, - null, 1, - null, - null, - null, - null, + 1, 1, null, null, - 1, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/unit/worker_appraise_spec.rb": { + "lines": [ + null, null, 1, 1, 1, + null, 1, 1, - 1, + 7, + 7, + null, + null, + 7, null, - 1, - 3, - 6, null, null, - 3, null, null, null, null, - 1, null, - 1, null, null, null, null, - 1, null, null, + 7, + 7, null, null, 1, + 7, null, null, 1, 1, + 1, null, null, 1, + null, 1, 1, 1, 1, 1, null, - 1, - 3, - 6, - null, - null, - 3, - null, - null, - null, null, 1, - null, + 1, 1, null, - null, + 1, null, null, 1, 1, null, - null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/integration/services/appraise_project_spec.rb": { - "lines": [ null, null, 1, 1, - 1, - null, - 1, null, 1, 1, null, - 1, - 2, null, null, 1, - 2, - null, - null, 1, 1, - 2, + 1, null, null, 1, - null, + 1, 1, null, null, @@ -3152,14 +4037,8 @@ null, null, 1, - null, - null, - null, - null, - null, - null, 1, - null, + 1, null, null, null, @@ -3174,41 +4053,28 @@ 1, null, null, - 1, - null, - null, - 1, - null, null, 1, - null, - null, - null, 1, + 2, null, null, null, - 1, null, null, null, + 2, null, null, null, - 1, null, null, null, null, null, - 1, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/integration/services/list_projects_spec.rb": { - "lines": [ + 2, null, null, 1, @@ -3216,236 +4082,182 @@ 1, null, 1, - null, - 1, 1, - null, - 1, - 3, - null, - null, 1, - 3, null, null, 1, 1, - 3, - null, null, 1, null, - 1, null, null, 1, + 1, + 1, null, + null + ] + }, + "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/workers/application/services/appraise_project.rb": { + "lines": [ null, null, 1, null, - null, 1, null, null, + 1, + 1, null, 1, 1, 1, - null, - null, 1, null, 1, null, null, - 1, - null, null, null, - 1, - 1, null, null, null, null, 1, - 1, - 1, null, + 0, + 0, + null, + 0, + 0, null, - 1, null, 1, + 0, null, + 0, + 0, null, + 0, null, + 0, + 0, null, - 1, null, null, + 0, null, null, - 1, - 1, - 1, + 0, null, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/unit/gateway_git_spec.rb": { - "lines": [ null, null, - 1, null, - 1, - 1, null, - 1, - 1, - 1, + 0, null, null, + 1, null, + 0, null, + 0, null, - 1, + 0, null, null, - 1, - 1, null, + 0, null, null, + 0, null, - 1, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/unit/local_cache_spec.rb": { - "lines": [ null, null, - 1, - 1, null, - 1, - 1, + 0, null, null, - 5, - 5, - 5, + 0, null, null, null, null, null, - 1, null, null, - 5, + 0, null, null, 1, - 1, + 0, null, - 1, + 0, + 0, null, + 0, + 0, null, - 1, - 1, + 0, null, + 0, null, - 1, - 1, - 1, - 1, - 1, - 1, null, - 1, + 0, + 0, + 0, null, - 1, - 1, - 1, null, null, 1, - 1, + 4, null, - 1, null, - 1, null, null, - 1, - 1, null, null, - 1, - 1, - 1, - 1, - 1, - 1, null, + 4, + 4, null, - 1, null, null, 1, + 3, + 4, null, + 3, null, - 1, null, - 1, null, null, - 1, - 1, null, null, - 1, null, - 1, null, - null - ] - }, - "/Users/soumyaray/Sync/Dropbox/ossdev/classes/SOA-class/projects/soa2025/api-codepraise-2025/spec/tests/unit/result_spec.rb": { - "lines": [ null, null, - 1, null, - 1, - 1, - 1, null, 1, - 1, + 4, null, null, - 1, - 1, null, - 1, - 1, null, null, - 1, - 1, - 1, null, null, null ] } }, - "timestamp": 1764941503 + "timestamp": 1766390257 } } diff --git a/require_worker.rb b/require_worker.rb new file mode 100644 index 0000000..7ae1c41 --- /dev/null +++ b/require_worker.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# Requires all ruby files in specified worker folders +# Worker has its own DDD layers parallel to the API: +# - domain: contributions entities/values/lib +# - infrastructure: git gateway/repositories/mappers, messaging +# - presentation: progress monitor values +# - application: controllers, services, requests +# Params: +# - (opt) folders: Array of folder names within workers/, or String of single folder name +# Usage: +# require_worker +# require_worker(%w[domain infrastructure]) +# require_worker('application') +def require_worker(folders = %w[domain infrastructure presentation application]) + worker_list = Array(folders).map { |folder| "workers/#{folder}" } + + Dir.glob("./{#{worker_list.join(',')}}/**/*.rb").each do |file| + require file + end +end diff --git a/spec/acceptance_tests b/spec/acceptance_tests index 5c77fc1..3e8ddf0 100755 --- a/spec/acceptance_tests +++ b/spec/acceptance_tests @@ -2,9 +2,26 @@ # see: https://stackoverflow.com/questions/360201/how-do-i-kill-background-processes-jobs-when-my-shell-script-exits trap "kill 0" EXIT -# Run application server as backround process (using '&') +# Ensure cache (Redis) is running +echo "Checking cache..." +bundle exec rake cache:ensure + +# Create logs directory if needed (gitignored via spec/logs/) +mkdir -p spec/logs + +# Run worker as background process, redirecting output to log file # see: https://kb.iu.edu/d/afnz -rake worker:run:test & +echo "Starting worker (output in spec/logs/worker.log)..." +rake worker:run:test > spec/logs/worker.log 2>&1 & -# Run acceptance tests on browser +# Give worker time to start +sleep 2 + +# Run acceptance tests +echo "Running tests..." bundle exec rake spec + +# Show worker log summary +echo "" +echo "=== Worker Log Summary ===" +grep -E "(INFO:|ERROR:|Progress:.*100)" spec/logs/worker.log 2>/dev/null | tail -20 || echo "No worker log found" diff --git a/spec/helpers/cache_helper.rb b/spec/helpers/cache_helper.rb new file mode 100644 index 0000000..311e25e --- /dev/null +++ b/spec/helpers/cache_helper.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# Helper for Redis cache testing +# Environment isolation via separate Redis databases: +# - Test uses redis://localhost:6379/1 (configured in secrets.yml) +# - Development uses redis://localhost:6379/0 +module CacheHelper + # Create a cache instance for testing + def self.create_test_cache + CodePraise::Cache::Remote.new(CodePraise::App.config) + end + + # Wipe all keys from test cache + def self.wipe_cache(cache = nil) + cache ||= create_test_cache + cache.wipe + end +end diff --git a/spec/helpers/spec_helper.rb b/spec/helpers/spec_helper.rb index 5770107..31d2d81 100644 --- a/spec/helpers/spec_helper.rb +++ b/spec/helpers/spec_helper.rb @@ -14,7 +14,10 @@ require 'webmock' require_relative '../../require_app' -require_app +require_relative '../../require_worker' + +require_app # Load API layers +require_worker(%w[domain infrastructure presentation]) # Load worker layers for tests (no application) USERNAME = 'soumyaray' PROJECT_NAME = 'YPBT-app' diff --git a/spec/tests/acceptance/api_spec.rb b/spec/tests/acceptance/api_spec.rb index b13555e..f4c84ae 100644 --- a/spec/tests/acceptance/api_spec.rb +++ b/spec/tests/acceptance/api_spec.rb @@ -3,6 +3,7 @@ require_relative '../../helpers/spec_helper' require_relative '../../helpers/vcr_helper' require_relative '../../helpers/database_helper' +require_relative '../../helpers/cache_helper' require 'rack/test' def app @@ -18,10 +19,12 @@ def app VcrHelper.configure_vcr_for_github DatabaseHelper.wipe_database CodePraise::Repository::RepoStore.wipe + CacheHelper.wipe_cache end after do VcrHelper.eject_vcr + CacheHelper.wipe_cache end describe 'Root route' do @@ -49,7 +52,8 @@ def app get "/api/v1/projects/#{USERNAME}/#{PROJECT_NAME}" _(last_response.status).must_equal 200 appraisal = JSON.parse last_response.body - _(appraisal.keys.sort).must_equal %w[folder project] + _(appraisal['status']).must_equal 'ok' + _(appraisal['folder_path']).must_equal '' _(appraisal['project']['name']).must_equal PROJECT_NAME _(appraisal['project']['owner']['username']).must_equal USERNAME _(appraisal['project']['contributors'].count).must_equal 3 @@ -72,7 +76,8 @@ def app get "/api/v1/projects/#{USERNAME}/#{PROJECT_NAME}/spec" _(last_response.status).must_equal 200 appraisal = JSON.parse last_response.body - _(appraisal.keys.sort).must_equal %w[folder project] + _(appraisal['status']).must_equal 'ok' + _(appraisal['folder_path']).must_equal 'spec' _(appraisal['project']['name']).must_equal PROJECT_NAME _(appraisal['project']['owner']['username']).must_equal USERNAME _(appraisal['project']['contributors'].count).must_equal 3 @@ -92,9 +97,12 @@ def app 5.times { sleep(1); print('_') } + # Error appraisals are cached with status 'error' get "/api/v1/projects/#{USERNAME}/#{PROJECT_NAME}/foobar" - _(last_response.status).must_equal 404 - _(JSON.parse(last_response.body)['status']).must_include 'not' + _(last_response.status).must_equal 200 + appraisal = JSON.parse last_response.body + _(appraisal['status']).must_equal 'error' + _(appraisal['error_type']).wont_be_nil end it 'should be report error for an invalid project' do diff --git a/spec/tests/integration/services/appraise_project_spec.rb b/spec/tests/integration/services/appraise_project_spec.rb deleted file mode 100644 index e9bd6a2..0000000 --- a/spec/tests/integration/services/appraise_project_spec.rb +++ /dev/null @@ -1,88 +0,0 @@ -# frozen_string_literal: true - -require_relative '../../../helpers/spec_helper' -require_relative '../../../helpers/vcr_helper' -require_relative '../../../helpers/database_helper' - -require 'ostruct' - -describe 'AppraiseProject Service Integration Test' do - VcrHelper.setup_vcr - - before do - VcrHelper.configure_vcr_for_github(recording: :none) - end - - after do - VcrHelper.eject_vcr - end - - describe 'Appraise a Project' do - before do - DatabaseHelper.wipe_database - end - - it 'HAPPY: should give contributions for a folder of an existing project' do - # GIVEN: a valid project that exists locally - gh_project = CodePraise::Github::ProjectMapper - .new(GITHUB_TOKEN) - .find(USERNAME, PROJECT_NAME) - CodePraise::Repository::For.entity(gh_project).create(gh_project) - gitrepo = CodePraise::GitRepo.new(gh_project, CodePraise::App.config) - gitrepo.clone_locally unless gitrepo.exists_locally? - - # WHEN: we request to appraise the project - request = OpenStruct.new( - owner_name: USERNAME, - project_name: PROJECT_NAME, - project_fullname: "#{USERNAME}/#{PROJECT_NAME}", - folder_name: '' - ) - - appraisal = CodePraise::Service::AppraiseProject.new.call( - requested: request, - config: CodePraise::App.config - ).value!.message - - # THEN: we should get an appraisal - folder = appraisal[:folder] - _(folder).must_be_kind_of CodePraise::Entity::FolderContributions - _(folder.subfolders.count).must_equal 10 - _(folder.base_files.count).must_equal 2 - - first_file = folder.base_files.first - _(%w[init.rb README.md]).must_include first_file.file_path.filename - _(folder.subfolders.first.path.size).must_be :>, 0 - - subfolders_plus_basefiles = - folder.subfolders.map(&:credit_share).reduce(&:+) + - folder.base_files.map(&:credit_share).reduce(&:+) - - _(subfolders_plus_basefiles.share.values.sort) - .must_equal(folder.credit_share.share.values.sort) - - _(subfolders_plus_basefiles.contributors.map(&:email).sort) - .must_equal(folder.credit_share.contributors.map(&:email).sort) - end - - it 'SAD: should not give contributions for non-existent project' do - # GIVEN: no project exists locally - - # WHEN: we request to appraise the project - request = OpenStruct.new( - owner_name: USERNAME, - project_name: PROJECT_NAME, - project_fullname: "#{USERNAME}/#{PROJECT_NAME}", - folder_name: '' - ) - - result = CodePraise::Service::AppraiseProject.new.call( - requested: request, - config: CodePraise::App.config - ) - - # THEN: we should get failure - _(result.failure?).must_equal true - end - end -end diff --git a/spec/tests/integration/services/fetch_or_request_appraisal_spec.rb b/spec/tests/integration/services/fetch_or_request_appraisal_spec.rb new file mode 100644 index 0000000..ff29df5 --- /dev/null +++ b/spec/tests/integration/services/fetch_or_request_appraisal_spec.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true + +require_relative '../../../helpers/spec_helper' +require_relative '../../../helpers/vcr_helper' +require_relative '../../../helpers/database_helper' +require_relative '../../../helpers/cache_helper' + +require 'ostruct' + +describe 'FetchOrRequestAppraisal Service Integration Test' do + VcrHelper.setup_vcr + + before do + VcrHelper.configure_vcr_for_github(recording: :none) + DatabaseHelper.wipe_database + @cache = CacheHelper.create_test_cache + CacheHelper.wipe_cache(@cache) + # Use App.config so the service uses the same Redis database as our test cache + @config = CodePraise::App.config + end + + after do + VcrHelper.eject_vcr + CacheHelper.wipe_cache(@cache) + end + + describe 'Fetch or Request Appraisal' do + it 'HAPPY: should return cached JSON when cache hit' do + # GIVEN: a valid project in database and cached appraisal + gh_project = CodePraise::Github::ProjectMapper + .new(GITHUB_TOKEN) + .find(USERNAME, PROJECT_NAME) + CodePraise::Repository::For.entity(gh_project).create(gh_project) + + # Pre-populate cache with appraisal JSON + cache_key = "appraisal:#{USERNAME}/#{PROJECT_NAME}/" + cached_json = '{"status":"ok","data":{"path":"","subfolders":[]}}' + @cache.set(cache_key, cached_json, ttl: 86_400) + + # WHEN: we request appraisal + request = OpenStruct.new( + owner_name: USERNAME, + project_name: PROJECT_NAME, + project_fullname: "#{USERNAME}/#{PROJECT_NAME}", + folder_name: '' + ) + + result = CodePraise::Service::FetchOrRequestAppraisal.new.call( + requested: request, + request_id: 'test-123', + config: @config + ) + + # THEN: we should get success with cached JSON + _(result.success?).must_equal true + _(result.value![:cache_hit]).must_equal true + _(result.value![:cached_json]).must_equal cached_json + end + + it 'HAPPY: should return processing status when cache miss' do + # GIVEN: a valid project in database but NO cached appraisal + gh_project = CodePraise::Github::ProjectMapper + .new(GITHUB_TOKEN) + .find(USERNAME, PROJECT_NAME) + CodePraise::Repository::For.entity(gh_project).create(gh_project) + + # WHEN: we request appraisal (cache is empty) + request = OpenStruct.new( + owner_name: USERNAME, + project_name: PROJECT_NAME, + project_fullname: "#{USERNAME}/#{PROJECT_NAME}", + folder_name: '' + ) + + # Mock the queue to avoid actual SQS calls + mock_queue = Minitest::Mock.new + mock_queue.expect(:send, nil, [String]) + + CodePraise::Messaging::Queue.stub(:new, mock_queue) do + result = CodePraise::Service::FetchOrRequestAppraisal.new.call( + requested: request, + request_id: 'test-123', + config: @config + ) + + # THEN: we should get failure with processing status + _(result.failure?).must_equal true + _(result.failure.status).must_equal :processing + _(result.failure.message[:request_id]).must_equal 'test-123' + end + + mock_queue.verify + end + + it 'SAD: should not give appraisal for non-existent project' do + # GIVEN: no project exists in database + + # WHEN: we request appraisal + request = OpenStruct.new( + owner_name: USERNAME, + project_name: PROJECT_NAME, + project_fullname: "#{USERNAME}/#{PROJECT_NAME}", + folder_name: '' + ) + + result = CodePraise::Service::FetchOrRequestAppraisal.new.call( + requested: request, + request_id: 'test-123', + config: @config + ) + + # THEN: we should get failure with not_found status + _(result.failure?).must_equal true + _(result.failure.status).must_equal :not_found + end + + it 'SAD: should reject too large projects' do + # GIVEN: a project that is too large + gh_project = CodePraise::Github::ProjectMapper + .new(GITHUB_TOKEN) + .find(USERNAME, PROJECT_NAME) + + # Create a mock project that is too large + large_project = CodePraise::Entity::Project.new( + id: nil, + origin_id: gh_project.origin_id, + name: gh_project.name, + size: 100_001, # Over 100MB limit + ssh_url: gh_project.ssh_url, + http_url: gh_project.http_url, + owner: gh_project.owner, + contributors: gh_project.contributors + ) + CodePraise::Repository::For.entity(large_project).create(large_project) + + # WHEN: we request appraisal + request = OpenStruct.new( + owner_name: USERNAME, + project_name: PROJECT_NAME, + project_fullname: "#{USERNAME}/#{PROJECT_NAME}", + folder_name: '' + ) + + result = CodePraise::Service::FetchOrRequestAppraisal.new.call( + requested: request, + request_id: 'test-123', + config: @config + ) + + # THEN: we should get failure with forbidden status + _(result.failure?).must_equal true + _(result.failure.status).must_equal :forbidden + end + end +end diff --git a/spec/tests/unit/appraisal_spec.rb b/spec/tests/unit/appraisal_spec.rb new file mode 100644 index 0000000..cab3377 --- /dev/null +++ b/spec/tests/unit/appraisal_spec.rb @@ -0,0 +1,194 @@ +# frozen_string_literal: true + +require_relative '../../helpers/spec_helper' + +describe 'Unit test of Value::Appraisal' do + before do + # Create minimal test fixtures + @owner = CodePraise::Entity::Member.new( + id: nil, + origin_id: 123, + username: 'testowner', + email: 'test@example.com' + ) + + @project = CodePraise::Entity::Project.new( + id: nil, + origin_id: 456, + name: 'test-project', + size: 100, + ssh_url: 'git@github.com:testowner/test-project.git', + http_url: 'https://github.com/testowner/test-project', + owner: @owner, + contributors: [] + ) + end + + describe 'success factory method' do + before do + # Create a minimal folder contributions entity for success case + # FolderContributions takes path: and files: (array of FileContributions) + @folder = CodePraise::Entity::FolderContributions.new( + path: '', + files: [] + ) + + @appraisal = CodePraise::Value::Appraisal.success( + project: @project, + folder_path: '', + folder: @folder + ) + end + + it 'should create a success appraisal' do + _(@appraisal.success?).must_equal true + _(@appraisal.error?).must_equal false + end + + it 'should have :ok status' do + _(@appraisal.status).must_equal :ok + end + + it 'should include project' do + _(@appraisal.project).must_equal @project + end + + it 'should include folder contributions' do + _(@appraisal.folder).must_equal @folder + end + + it 'should have nil error fields' do + _(@appraisal.error_type).must_be_nil + _(@appraisal.error_message).must_be_nil + end + + it 'should generate correct cache key' do + _(@appraisal.cache_key).must_equal 'appraisal:testowner/test-project/' + end + + it 'should return success TTL' do + _(@appraisal.ttl).must_equal CodePraise::Value::Appraisal::SUCCESS_TTL + _(@appraisal.ttl).must_equal 86_400 + end + end + + describe 'error factory method' do + before do + @appraisal = CodePraise::Value::Appraisal.error( + project: @project, + folder_path: 'nonexistent/path', + error_type: 'not_found', + error_message: 'Folder does not exist' + ) + end + + it 'should create an error appraisal' do + _(@appraisal.error?).must_equal true + _(@appraisal.success?).must_equal false + end + + it 'should have :error status' do + _(@appraisal.status).must_equal :error + end + + it 'should include project' do + _(@appraisal.project).must_equal @project + end + + it 'should have nil folder' do + _(@appraisal.folder).must_be_nil + end + + it 'should include error details' do + _(@appraisal.error_type).must_equal 'not_found' + _(@appraisal.error_message).must_equal 'Folder does not exist' + end + + it 'should generate correct cache key with folder path' do + _(@appraisal.cache_key).must_equal 'appraisal:testowner/test-project/nonexistent/path' + end + + it 'should return error TTL' do + _(@appraisal.ttl).must_equal CodePraise::Value::Appraisal::ERROR_TTL + _(@appraisal.ttl).must_equal 10 + end + end + + describe 'immutability' do + it 'should not allow attribute modification' do + appraisal = CodePraise::Value::Appraisal.error( + project: @project, + folder_path: '', + error_type: 'test', + error_message: 'test' + ) + + # Dry::Struct instances are immutable - no setter methods exist + _(appraisal.respond_to?(:status=)).must_equal false + _(appraisal.respond_to?(:error_type=)).must_equal false + end + end +end + +describe 'Unit test of Representer::Appraisal' do + before do + @owner = CodePraise::Entity::Member.new( + id: nil, + origin_id: 123, + username: 'testowner', + email: 'test@example.com' + ) + + @project = CodePraise::Entity::Project.new( + id: nil, + origin_id: 456, + name: 'test-project', + size: 100, + ssh_url: 'git@github.com:testowner/test-project.git', + http_url: 'https://github.com/testowner/test-project', + owner: @owner, + contributors: [] + ) + end + + # Note: Full success case serialization with FolderContributions is tested + # in integration tests. Unit tests focus on the Appraisal wrapper behavior. + + describe 'error case serialization' do + before do + @appraisal = CodePraise::Value::Appraisal.error( + project: @project, + folder_path: 'bad/path', + error_type: 'not_found', + error_message: 'Folder not found' + ) + + @json = CodePraise::Representer::Appraisal.new(@appraisal).to_json + @parsed = JSON.parse(@json) + end + + it 'should serialize status as string' do + _(@parsed['status']).must_equal 'error' + end + + it 'should include project' do + _(@parsed['project']).wont_be_nil + end + + it 'should include folder_path' do + _(@parsed['folder_path']).must_equal 'bad/path' + end + + it 'should NOT include folder' do + _(@parsed.key?('folder')).must_equal false + end + + it 'should include error_type' do + _(@parsed['error_type']).must_equal 'not_found' + end + + it 'should include message (aliased from error_message)' do + _(@parsed['message']).must_equal 'Folder not found' + end + end +end diff --git a/spec/tests/unit/local_cache_spec.rb b/spec/tests/unit/local_cache_spec.rb deleted file mode 100644 index 3be3f3f..0000000 --- a/spec/tests/unit/local_cache_spec.rb +++ /dev/null @@ -1,87 +0,0 @@ -# frozen_string_literal: true - -require_relative '../../helpers/spec_helper' -require 'fileutils' - -describe 'Unit test of LocalCache' do - before do - # SAFETY: Create a mock config that returns a TEST cache directory - # This prevents tests from touching the real cache at _cache/rack - @test_cache_dir = 'spec/fixtures/test_cache' - @config = Minitest::Mock.new - @config.expect(:LOCAL_CACHE, @test_cache_dir) - - # When LocalCache.new(@config) is called, it will use @test_cache_dir - # instead of the real LOCAL_CACHE value from secrets.yml - end - - after do - # Clean up the temporary test cache directory after each test - # The real cache directory (_cache/rack) is never touched - FileUtils.rm_rf(@test_cache_dir) - end - - it 'should create cache directory if it does not exist' do - CodePraise::Cache::Local.new(@config) - - _(Dir.exist?(@test_cache_dir)).must_equal true - end - - it 'should list keys when cache has files' do - cache = CodePraise::Cache::Local.new(@config) - - # Create some test files in the TEST directory - meta_dir = "#{@test_cache_dir}/meta" - body_dir = "#{@test_cache_dir}/body" - FileUtils.mkdir_p(meta_dir) - FileUtils.mkdir_p(body_dir) - FileUtils.touch("#{meta_dir}/test1") - FileUtils.touch("#{body_dir}/test2") - - keys = cache.keys - - _(keys.length).must_equal 2 - _(keys).must_include "#{meta_dir}/test1" - _(keys).must_include "#{body_dir}/test2" - end - - it 'should return empty array when no cache files exist' do - cache = CodePraise::Cache::Local.new(@config) - - keys = cache.keys - - _(keys).must_be_empty - end - - it 'should wipe all cache files' do - cache = CodePraise::Cache::Local.new(@config) - - # Create some test files in the TEST directory - meta_dir = "#{@test_cache_dir}/meta" - body_dir = "#{@test_cache_dir}/body" - FileUtils.mkdir_p(meta_dir) - FileUtils.mkdir_p(body_dir) - FileUtils.touch("#{meta_dir}/test1") - FileUtils.touch("#{body_dir}/test2") - - # Verify files exist before wipe - _(cache.keys.length).must_equal 2 - - # Wipe cache - only affects TEST directory, not real cache - cache.wipe - - # Verify files are gone - _(cache.keys).must_be_empty - # But directory should still exist (wipe removes contents, not directory) - _(Dir.exist?(@test_cache_dir)).must_equal true - end - - it 'should handle wipe when cache is already empty' do - cache = CodePraise::Cache::Local.new(@config) - - # Should not raise error when wiping empty cache - cache.wipe - - _(cache.keys).must_be_empty - end -end diff --git a/spec/tests/unit/remote_cache_spec.rb b/spec/tests/unit/remote_cache_spec.rb new file mode 100644 index 0000000..c5f8121 --- /dev/null +++ b/spec/tests/unit/remote_cache_spec.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +require_relative '../../helpers/spec_helper' +require_relative '../../helpers/cache_helper' + +describe 'Unit test of Cache::Remote' do + before do + @cache = CacheHelper.create_test_cache + CacheHelper.wipe_cache(@cache) + end + + after do + CacheHelper.wipe_cache(@cache) + end + + describe 'set and get' do + it 'should store and retrieve a value' do + @cache.set('test:key', 'hello world', ttl: 3600) + + _(@cache.get('test:key')).must_equal 'hello world' + end + + it 'should return nil for non-existent key' do + _(@cache.get('nonexistent')).must_be_nil + end + + it 'should store JSON strings' do + json = '{"status":"ok","data":{"count":42}}' + @cache.set('json:key', json, ttl: 3600) + + result = @cache.get('json:key') + _(result).must_equal json + _(JSON.parse(result)['data']['count']).must_equal 42 + end + end + + describe 'exists?' do + it 'should return true for existing key' do + @cache.set('exists:key', 'value', ttl: 3600) + + _(@cache.exists?('exists:key')).must_equal true + end + + it 'should return false for non-existent key' do + _(@cache.exists?('nonexistent')).must_equal false + end + end + + describe 'keys' do + it 'should list all keys in the database' do + @cache.set('key1', 'value1', ttl: 3600) + @cache.set('key2', 'value2', ttl: 3600) + + keys = @cache.keys + # Environment isolation via separate Redis databases (no key prefixes) + _(keys).must_include 'key1' + _(keys).must_include 'key2' + _(keys.length).must_equal 2 + end + end + + describe 'wipe' do + it 'should remove all keys' do + @cache.set('key1', 'value1', ttl: 3600) + @cache.set('key2', 'value2', ttl: 3600) + + @cache.wipe + + _(@cache.keys).must_be_empty + _(@cache.exists?('key1')).must_equal false + end + end +end diff --git a/spec/tests/unit/worker_appraise_spec.rb b/spec/tests/unit/worker_appraise_spec.rb new file mode 100644 index 0000000..6ec557a --- /dev/null +++ b/spec/tests/unit/worker_appraise_spec.rb @@ -0,0 +1,147 @@ +# frozen_string_literal: true + +require_relative '../../helpers/spec_helper' +require_relative '../../helpers/cache_helper' +require_relative '../../../workers/application/services/appraise_project' + +describe 'Unit test of Appraiser::Service::AppraiseProject' do + before do + @cache = CacheHelper.create_test_cache + CacheHelper.wipe_cache(@cache) + + # Create test project OpenStruct (simulating deserialized JSON) + @project_ostruct = OpenStruct.new( + origin_id: 123, + name: 'test-project', + size: 100, + ssh_url: 'git@github.com:testowner/test-project.git', + http_url: 'https://github.com/testowner/test-project', + fullname: 'testowner/test-project', + owner: OpenStruct.new( + origin_id: 456, + username: 'testowner', + email: 'test@example.com' + ), + contributors: [] + ) + + @progress_calls = [] + @progress_callback = ->(percent) { @progress_calls << percent } + end + + after do + CacheHelper.wipe_cache(@cache) + end + + describe 'build_project_entity helper' do + it 'should convert OpenStruct to Entity::Project' do + service = Appraiser::Service::AppraiseProject.new + + # Access private method for testing + entity = service.send(:build_project_entity, @project_ostruct) + + _(entity).must_be_instance_of CodePraise::Entity::Project + _(entity.name).must_equal 'test-project' + _(entity.origin_id).must_equal 123 + _(entity.owner.username).must_equal 'testowner' + _(entity.fullname).must_equal 'testowner/test-project' + end + + it 'should handle empty contributors' do + service = Appraiser::Service::AppraiseProject.new + entity = service.send(:build_project_entity, @project_ostruct) + + _(entity.contributors).must_equal [] + end + + it 'should convert contributors' do + @project_ostruct.contributors = [ + OpenStruct.new(origin_id: 789, username: 'contributor1', email: 'c1@example.com') + ] + + service = Appraiser::Service::AppraiseProject.new + entity = service.send(:build_project_entity, @project_ostruct) + + _(entity.contributors.length).must_equal 1 + _(entity.contributors.first.username).must_equal 'contributor1' + end + end + + describe 'scale_clone_progress helper' do + it 'should scale Cloning to 25' do + service = Appraiser::Service::AppraiseProject.new + _(service.send(:scale_clone_progress, 'Cloning into...')).must_equal 25 + end + + it 'should scale Receiving to 40' do + service = Appraiser::Service::AppraiseProject.new + _(service.send(:scale_clone_progress, 'Receiving objects: 50%')).must_equal 40 + end + + it 'should scale Checking to 50' do + service = Appraiser::Service::AppraiseProject.new + _(service.send(:scale_clone_progress, 'Checking connectivity...')).must_equal 50 + end + + it 'should default unknown stages to 30' do + service = Appraiser::Service::AppraiseProject.new + _(service.send(:scale_clone_progress, 'Unknown stage')).must_equal 30 + end + end +end + +describe 'Unit test of AppraisalRequest' do + it 'should create AppraisalRequest struct' do + project = OpenStruct.new(name: 'test') + request = CodePraise::Response::AppraisalRequest.new(project, 'app/models', 'request-123') + + _(request.project.name).must_equal 'test' + _(request.folder_path).must_equal 'app/models' + _(request.id).must_equal 'request-123' + end +end + +describe 'Unit test of Representer::AppraisalRequest' do + before do + @owner = CodePraise::Entity::Member.new( + id: nil, + origin_id: 123, + username: 'testowner', + email: 'test@example.com' + ) + + @project = CodePraise::Entity::Project.new( + id: nil, + origin_id: 456, + name: 'test-project', + size: 100, + ssh_url: 'git@github.com:testowner/test-project.git', + http_url: 'https://github.com/testowner/test-project', + owner: @owner, + contributors: [] + ) + + @request = CodePraise::Response::AppraisalRequest.new(@project, 'app/models', 'req-123') + end + + it 'should serialize to JSON' do + json = CodePraise::Representer::AppraisalRequest.new(@request).to_json + parsed = JSON.parse(json) + + _(parsed['folder_path']).must_equal 'app/models' + _(parsed['id']).must_equal 'req-123' + _(parsed['project']['name']).must_equal 'test-project' + end + + it 'should deserialize from JSON' do + json = CodePraise::Representer::AppraisalRequest.new(@request).to_json + + deserialized = CodePraise::Representer::AppraisalRequest + .new(OpenStruct.new) + .from_json(json) + + _(deserialized.folder_path).must_equal 'app/models' + _(deserialized.id).must_equal 'req-123' + _(deserialized.project.name).must_equal 'test-project' + end +end diff --git a/workers/git_clone_worker.rb b/workers/application/controllers/worker.rb similarity index 53% rename from workers/git_clone_worker.rb rename to workers/application/controllers/worker.rb index e63270d..537ef23 100644 --- a/workers/git_clone_worker.rb +++ b/workers/application/controllers/worker.rb @@ -1,15 +1,16 @@ # frozen_string_literal: true -require_relative '../require_app' -require_relative 'clone_monitor' -require_relative 'job_reporter' -require_app +require_relative '../../../require_app' +require_relative '../../../require_worker' + +require_app # Load API layers (domain, infrastructure, presentation, application) +require_worker # Load worker-only layers (domain, infrastructure, presentation, application) require 'figaro' require 'shoryuken' -module GitClone - # Shoryuken worker class to clone repos in parallel +module Appraiser + # Shoryuken worker class to clone repos and appraise contributions class Worker # Environment variables setup Figaro.application = Figaro::Application.new( @@ -28,21 +29,31 @@ def self.config = Figaro.env include Shoryuken::Worker Shoryuken.sqs_client_receive_message_opts = { wait_time_seconds: 20 } - shoryuken_options queue: config.CLONE_QUEUE_URL, auto_delete: true + shoryuken_options queue: config.WORKER_QUEUE_URL, auto_delete: true def perform(_sqs_msg, request) job = JobReporter.new(request, Worker.config) - - job.report(CloneMonitor.starting_percent) - CodePraise::GitRepo.new(job.project, Worker.config).clone_locally do |line| - job.report CloneMonitor.progress(line) - end - - # Keep sending finished status to any latecoming subscribers - job.report_each_second(5) { CloneMonitor.finished_percent } + perform_appraisal(job) rescue CodePraise::GitRepo::Errors::CannotOverwriteLocalGitRepo # worker should crash fail early - only catch errors we expect! puts 'CLONE EXISTS -- ignoring request' end + + private + + def perform_appraisal(job) + gitrepo = CodePraise::GitRepo.new(job.project, Worker.config) + + Service::AppraiseProject.new.call( + project: job.project, + folder_path: job.folder_path, + config: Worker.config, + gitrepo: gitrepo, + progress: job.progress_callback + ) + + # Keep sending finished status to any latecoming subscribers + job.report_each_second(5) { AppraisalMonitor.finished_percent } + end end end diff --git a/workers/application/requests/job_reporter.rb b/workers/application/requests/job_reporter.rb new file mode 100644 index 0000000..686ecd2 --- /dev/null +++ b/workers/application/requests/job_reporter.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +# Note: ProgressPublisher is loaded via require_worker (infrastructure/messaging) + +module Appraiser + # Reports job progress to client + class JobReporter + attr_reader :project, :folder_path + + def initialize(request_json, config) + request = parse_request(request_json) + + @project = request.project + @folder_path = request.respond_to?(:folder_path) ? (request.folder_path || '') : '' + @publisher = ProgressPublisher.new(config, request.id) + end + + def report(msg) + @publisher.publish msg + end + + def report_each_second(seconds, &operation) + seconds.times do + sleep(1) + report(operation.call) + end + end + + # Returns a proc that can be passed to services for progress reporting + def progress_callback + ->(percent) { report(percent.to_s) } + end + + private + + # Parse request - handles both CloneRequest and AppraisalRequest formats + def parse_request(request_json) + parsed = JSON.parse(request_json) + + if parsed.key?('folder_path') + # New AppraisalRequest format + CodePraise::Representer::AppraisalRequest + .new(OpenStruct.new) + .from_json(request_json) + else + # Legacy CloneRequest format + CodePraise::Representer::CloneRequest + .new(OpenStruct.new) + .from_json(request_json) + end + end + end +end diff --git a/workers/application/services/appraise_project.rb b/workers/application/services/appraise_project.rb new file mode 100644 index 0000000..1db3382 --- /dev/null +++ b/workers/application/services/appraise_project.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true + +require 'dry/transaction' + +module Appraiser + module Service + # Service to clone repo, appraise contributions, and cache result + # Uses Dry::Transaction for composable steps with railway-oriented error handling + class AppraiseProject + include Dry::Transaction + + step :prepare_inputs + step :clone_repo + step :appraise_contributions + step :cache_result + + # Input: { project:, folder_path:, config:, gitrepo:, progress: } + # project: OpenStruct from deserializing AppraisalRequest + # folder_path: String path to folder being appraised + # config: Worker config with Redis URL + # gitrepo: GitRepo instance + # progress: Proc to report progress + + def prepare_inputs(input) + # Convert OpenStruct project to Entity::Project for Value::Appraisal + input[:project_entity] = build_project_entity(input[:project]) + Success(input) + rescue StandardError => e + puts "PREPARE ERROR: #{e.message}" + Failure(input.merge(error: { type: 'prepare_failed', message: e.message })) + end + + def clone_repo(input) + input[:progress].call(15) # STARTED + + if input[:gitrepo].exists_locally? + input[:progress].call(50) # Skip to post-clone + else + input[:gitrepo].clone_locally do |line| + # Scale clone progress from 15 to 50 + percent = scale_clone_progress(line) + input[:progress].call(percent) + end + end + + Success(input) + rescue StandardError => e + # Create error appraisal and cache it + input[:appraisal] = CodePraise::Value::Appraisal.error( + project: input[:project_entity], + folder_path: input[:folder_path], + error_type: 'clone_failed', + error_message: e.message + ) + # Continue to cache the error + Success(input) + end + + def appraise_contributions(input) + # Skip if already errored in clone step + return Success(input) if input[:appraisal]&.error? + + input[:progress].call(55) # Starting appraisal + + folder = CodePraise::Mapper::Contributions + .new(input[:gitrepo]) + .for_folder(input[:folder_path]) + + input[:progress].call(85) # Appraisal complete + + # Build successful appraisal value object + input[:appraisal] = CodePraise::Value::Appraisal.success( + project: input[:project_entity], + folder_path: input[:folder_path], + folder: folder + ) + + Success(input) + rescue StandardError => e + # Create error appraisal + input[:appraisal] = CodePraise::Value::Appraisal.error( + project: input[:project_entity], + folder_path: input[:folder_path], + error_type: 'appraisal_failed', + error_message: e.message + ) + + # Still cache the error - return Success to continue to cache_result + Success(input) + end + + def cache_result(input) + input[:progress].call(90) # Caching + + appraisal = input[:appraisal] + json = CodePraise::Representer::Appraisal.new(appraisal).to_json + + cache = CodePraise::Cache::Remote.new(input[:config]) + cache.set(appraisal.cache_key, json, ttl: appraisal.ttl) + + input[:progress].call(100) # FINISHED + + Success(input) + rescue StandardError => e + # Cache failure - still report 100% so client can retry + puts "CACHE ERROR: #{e.message}" + input[:progress].call(100) + Success(input) + end + + private + + # Scale git clone progress (15-50 range) + def scale_clone_progress(line) + clone_stages = { + 'Cloning' => 25, + 'remote' => 35, + 'Receiving' => 40, + 'Resolving' => 45, + 'Checking' => 50 + } + + first_word = line.match(/^[A-Za-z]+/).to_s + clone_stages[first_word] || 30 + end + + # Convert OpenStruct project (from JSON) to Entity::Project + def build_project_entity(project_ostruct) + owner = build_member_entity(project_ostruct.owner) + contributors = (project_ostruct.contributors || []).map { |c| build_member_entity(c) } + + CodePraise::Entity::Project.new( + id: nil, + origin_id: project_ostruct.origin_id, + name: project_ostruct.name, + size: project_ostruct.size, + ssh_url: project_ostruct.ssh_url, + http_url: project_ostruct.http_url, + owner: owner, + contributors: contributors + ) + end + + # Convert OpenStruct member to Entity::Member + def build_member_entity(member_ostruct) + CodePraise::Entity::Member.new( + id: nil, + origin_id: member_ostruct.origin_id, + username: member_ostruct.username, + email: member_ostruct.email || '' + ) + end + end + end +end diff --git a/app/domain/contributions/entities/contributor.rb b/workers/domain/contributions/entities/contributor.rb similarity index 100% rename from app/domain/contributions/entities/contributor.rb rename to workers/domain/contributions/entities/contributor.rb diff --git a/app/domain/contributions/entities/file_contributions.rb b/workers/domain/contributions/entities/file_contributions.rb similarity index 100% rename from app/domain/contributions/entities/file_contributions.rb rename to workers/domain/contributions/entities/file_contributions.rb diff --git a/app/domain/contributions/entities/folder_contributions.rb b/workers/domain/contributions/entities/folder_contributions.rb similarity index 100% rename from app/domain/contributions/entities/folder_contributions.rb rename to workers/domain/contributions/entities/folder_contributions.rb diff --git a/app/domain/contributions/entities/line_contribution.rb b/workers/domain/contributions/entities/line_contribution.rb similarity index 100% rename from app/domain/contributions/entities/line_contribution.rb rename to workers/domain/contributions/entities/line_contribution.rb diff --git a/app/domain/contributions/lib/contributions_calculator.rb b/workers/domain/contributions/lib/contributions_calculator.rb similarity index 100% rename from app/domain/contributions/lib/contributions_calculator.rb rename to workers/domain/contributions/lib/contributions_calculator.rb diff --git a/app/domain/contributions/lib/types.rb b/workers/domain/contributions/lib/types.rb similarity index 100% rename from app/domain/contributions/lib/types.rb rename to workers/domain/contributions/lib/types.rb diff --git a/app/domain/contributions/values/code_language.rb b/workers/domain/contributions/values/code_language.rb similarity index 100% rename from app/domain/contributions/values/code_language.rb rename to workers/domain/contributions/values/code_language.rb diff --git a/app/domain/contributions/values/contributors.rb b/workers/domain/contributions/values/contributors.rb similarity index 100% rename from app/domain/contributions/values/contributors.rb rename to workers/domain/contributions/values/contributors.rb diff --git a/app/domain/contributions/values/credit_share.rb b/workers/domain/contributions/values/credit_share.rb similarity index 100% rename from app/domain/contributions/values/credit_share.rb rename to workers/domain/contributions/values/credit_share.rb diff --git a/app/domain/contributions/values/file_path.rb b/workers/domain/contributions/values/file_path.rb similarity index 100% rename from app/domain/contributions/values/file_path.rb rename to workers/domain/contributions/values/file_path.rb diff --git a/app/infrastructure/git/gateway/git_command.rb b/workers/infrastructure/git/gateway/git_command.rb similarity index 100% rename from app/infrastructure/git/gateway/git_command.rb rename to workers/infrastructure/git/gateway/git_command.rb diff --git a/app/infrastructure/git/mappers/blame_contributor.rb b/workers/infrastructure/git/mappers/blame_contributor.rb similarity index 100% rename from app/infrastructure/git/mappers/blame_contributor.rb rename to workers/infrastructure/git/mappers/blame_contributor.rb diff --git a/app/infrastructure/git/mappers/contributions_mapper.rb b/workers/infrastructure/git/mappers/contributions_mapper.rb similarity index 100% rename from app/infrastructure/git/mappers/contributions_mapper.rb rename to workers/infrastructure/git/mappers/contributions_mapper.rb diff --git a/app/infrastructure/git/mappers/file_contributions_mapper.rb b/workers/infrastructure/git/mappers/file_contributions_mapper.rb similarity index 100% rename from app/infrastructure/git/mappers/file_contributions_mapper.rb rename to workers/infrastructure/git/mappers/file_contributions_mapper.rb diff --git a/app/infrastructure/git/mappers/folder_contributions_mapper.rb b/workers/infrastructure/git/mappers/folder_contributions_mapper.rb similarity index 100% rename from app/infrastructure/git/mappers/folder_contributions_mapper.rb rename to workers/infrastructure/git/mappers/folder_contributions_mapper.rb diff --git a/app/infrastructure/git/mappers/porcelain_parser.rb b/workers/infrastructure/git/mappers/porcelain_parser.rb similarity index 100% rename from app/infrastructure/git/mappers/porcelain_parser.rb rename to workers/infrastructure/git/mappers/porcelain_parser.rb diff --git a/app/infrastructure/git/repositories/blame_reporter.rb b/workers/infrastructure/git/repositories/blame_reporter.rb similarity index 100% rename from app/infrastructure/git/repositories/blame_reporter.rb rename to workers/infrastructure/git/repositories/blame_reporter.rb diff --git a/app/infrastructure/git/repositories/git_repo.rb b/workers/infrastructure/git/repositories/git_repo.rb similarity index 100% rename from app/infrastructure/git/repositories/git_repo.rb rename to workers/infrastructure/git/repositories/git_repo.rb diff --git a/app/infrastructure/git/repositories/local_repo.rb b/workers/infrastructure/git/repositories/local_repo.rb similarity index 100% rename from app/infrastructure/git/repositories/local_repo.rb rename to workers/infrastructure/git/repositories/local_repo.rb diff --git a/app/infrastructure/git/repositories/remote_repo.rb b/workers/infrastructure/git/repositories/remote_repo.rb similarity index 100% rename from app/infrastructure/git/repositories/remote_repo.rb rename to workers/infrastructure/git/repositories/remote_repo.rb diff --git a/app/infrastructure/git/repositories/repo_file.rb b/workers/infrastructure/git/repositories/repo_file.rb similarity index 100% rename from app/infrastructure/git/repositories/repo_file.rb rename to workers/infrastructure/git/repositories/repo_file.rb diff --git a/app/infrastructure/git/repositories/repo_store.rb b/workers/infrastructure/git/repositories/repo_store.rb similarity index 100% rename from app/infrastructure/git/repositories/repo_store.rb rename to workers/infrastructure/git/repositories/repo_store.rb diff --git a/workers/progress_publisher.rb b/workers/infrastructure/messaging/progress_publisher.rb similarity index 97% rename from workers/progress_publisher.rb rename to workers/infrastructure/messaging/progress_publisher.rb index 5dd2d5a..beef60b 100644 --- a/workers/progress_publisher.rb +++ b/workers/infrastructure/messaging/progress_publisher.rb @@ -2,7 +2,7 @@ require 'http' -module GitClone +module Appraiser # Publishes progress as percent to Faye endpoint class ProgressPublisher def initialize(config, channel_id) diff --git a/workers/job_reporter.rb b/workers/job_reporter.rb deleted file mode 100644 index 0398582..0000000 --- a/workers/job_reporter.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -require_relative 'progress_publisher' - -module GitClone - # Reports job progress to client - class JobReporter - attr_accessor :project - - def initialize(request_json, config) - clone_request = CodePraise::Representer::CloneRequest - .new(OpenStruct.new) - .from_json(request_json) - - @project = clone_request.project - @publisher = ProgressPublisher.new(config, clone_request.id) - end - - def report(msg) - @publisher.publish msg - end - - def report_each_second(seconds, &operation) - seconds.times do - sleep(1) - report(operation.call) - end - end - end -end diff --git a/workers/clone_monitor.rb b/workers/presentation/values/progress_monitor.rb similarity index 52% rename from workers/clone_monitor.rb rename to workers/presentation/values/progress_monitor.rb index b93749e..cfd7ffd 100644 --- a/workers/clone_monitor.rb +++ b/workers/presentation/values/progress_monitor.rb @@ -1,7 +1,8 @@ # frozen_string_literal: true -module GitClone +module Appraiser # Infrastructure to clone while yielding progress + # Legacy module for backwards compatibility module CloneMonitor CLONE_PROGRESS = { 'STARTED' => 15, @@ -33,4 +34,31 @@ def self.first_word_of(line) line.match(/^[A-Za-z]+/).to_s end end + + # Progress phases for full appraisal flow (clone + appraise + cache) + # Progress distribution: + # Clone: 0-50% (or skip if already cloned) + # Appraise: 50-90% + # Cache: 90-100% + module AppraisalMonitor + PHASES = { + started: 15, + cloning: 25, + clone_receiving: 40, + clone_resolving: 45, + clone_done: 50, + appraising: 55, + appraise_done: 85, + caching: 90, + finished: 100 + }.freeze + + def self.starting_percent + PHASES[:started].to_s + end + + def self.finished_percent + PHASES[:finished].to_s + end + end end diff --git a/workers/shoryuken.yml b/workers/shoryuken.yml index 2a460f0..ecc8704 100644 --- a/workers/shoryuken.yml +++ b/workers/shoryuken.yml @@ -1,2 +1,2 @@ queues: - - https://sqs.us-east-1.amazonaws.com/503315808870/soa-codepraise-clone-production + - https://sqs.us-east-1.amazonaws.com/503315808870/soa-codepraise-worker-production diff --git a/workers/shoryuken_dev.yml b/workers/shoryuken_dev.yml index 35848dd..e994f08 100644 --- a/workers/shoryuken_dev.yml +++ b/workers/shoryuken_dev.yml @@ -1,2 +1,2 @@ queues: - - https://sqs.ap-northeast-1.amazonaws.com/503315808870/soa-codepraise-clone-dev + - https://sqs.ap-northeast-1.amazonaws.com/503315808870/soa-codepraise-worker-dev diff --git a/workers/shoryuken_test.yml b/workers/shoryuken_test.yml index be9aa3c..55dff4c 100644 --- a/workers/shoryuken_test.yml +++ b/workers/shoryuken_test.yml @@ -1,2 +1,2 @@ queues: - - https://sqs.ap-northeast-1.amazonaws.com/503315808870/soa-codepraise-clone-test + - https://sqs.ap-northeast-1.amazonaws.com/503315808870/soa-codepraise-worker-test