diff --git a/.dockerignore b/.dockerignore index 6672e96..cee8f5f 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,7 +7,6 @@ db/*.sqlite3.* docker/** .gitignore .hound.yml -.rubocop.yml docker-compose.yml README.md .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4db6544..b0c5c3a 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -13,6 +13,21 @@ include: - project: 'turniere/turniere-infra' file: '/ci/pipeline.yaml' +lint: + stage: test + tags: + - docker + needs: + - job: build_image + image: $CI_REGISTRY_IMAGE/test/commits:$CI_COMMIT_SHA + rules: + - if: $CI_PIPELINE_SOURCE != "push" + when: never + - when: always + script: + - cd /app + - inv lint + rails spec: stage: test tags: diff --git a/.rubocop.yml b/.rubocop.yml index 5a901d6..8981d87 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,9 +1,3 @@ -inherit_from: .rubocop_todo.yml - -inherit_mode: - merge: - - Exclude - plugins: - rubocop-performance - rubocop-rails @@ -15,6 +9,7 @@ AllCops: Exclude: - "bin/**/*" - "db/schema.rb" + - "spec/**/*.rb" # Enable Rails Rails: @@ -22,14 +17,14 @@ Rails: # Commonly used screens these days easily fit more than 80 characters. Layout/LineLength: - Max: 120 + Max: 140 # Too short methods lead to extraction of single-use methods, which can make # the code easier to read (by naming things), but can also clutter the class Metrics/MethodLength: Exclude: - "db/migrate/*" - Max: 50 + Max: 60 # The guiding principle of classes is SRP, SRP can't be accurately measured by LoC Metrics/ClassLength: @@ -41,9 +36,10 @@ Metrics/ModuleLength: Metrics/AbcSize: Exclude: - "db/migrate/*" - Max: 50 + Max: 75 Metrics/BlockLength: + Max: 30 AllowedMethods: - "namespace" - "create_table" @@ -51,6 +47,42 @@ Metrics/BlockLength: - "config/**/*.rb" - "spec/**/*.rb" +Metrics/CyclomaticComplexity: + Max: 22 + +Metrics/ParameterLists: + Max: 7 + +Metrics/PerceivedComplexity: + Max: 24 + +Rails/ReversibleMigration: + Exclude: + - "db/migrate/**/*" + +Rails/SkipsModelValidations: + Exclude: + - "app/models/tournament_sync_queue_entry.rb" + - "app/services/tournament_sync_pusher.rb" + - "db/migrate/**/*" + +# These are standalone CLI entrypoints where stdout is command output, +# not Rails application logging. +Rails/Output: + Exclude: + - "e2e/load_test.rb" + - "script/e2e_scenarios.rb" + - "script/wait_for_http.rb" + +Rails/Delegate: + Exclude: + - "app/controllers/tournaments_controller.rb" + - "e2e/lib/action_cable_client.rb" + +Style/MultilineBlockChain: + Exclude: + - "e2e/lib/api_client.rb" + # Disable documentation checks for now # should be done with yard instead Style/Documentation: diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml deleted file mode 100644 index de039d0..0000000 --- a/.rubocop_todo.yml +++ /dev/null @@ -1,563 +0,0 @@ -# This configuration was generated by -# `rubocop --auto-gen-config --no-exclude-limit` -# on 2026-04-28 09:58:15 UTC using RuboCop version 1.86.1. -# The point is for the user to remove these configuration records -# one by one as the offenses are removed from the code base. -# Note that changes in the inspected code, or installation of new -# versions of RuboCop, may require this file to be generated again. - -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation. -Bundler/OrderedGems: - Exclude: - - 'Gemfile' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyleAlignWith. -# SupportedStylesAlignWith: either, start_of_block, start_of_line -Layout/BlockAlignment: - Exclude: - - 'spec/routing/tournaments_routing_spec.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowForAlignment. -Layout/CommentIndentation: - Exclude: - - 'app/serializers/tournament_serializer.rb' - -# Offense count: 3 -# This cop supports safe autocorrection (--autocorrect). -Layout/EmptyLineAfterGuardClause: - Exclude: - - 'app/controllers/tournaments_controller.rb' - - 'config/spring.rb' - - 'spec/factories/tournaments.rb' - -# Offense count: 3 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EmptyLineBetweenMethodDefs, EmptyLineBetweenClassDefs, EmptyLineBetweenModuleDefs, DefLikeMacros, AllowAdjacentOneLineDefs, NumberOfEmptyLines. -Layout/EmptyLineBetweenDefs: - Exclude: - - 'app/controllers/tournaments_controller.rb' - - 'app/services/tournament_snapshot_importer.rb' - - 'e2e/lib/scenario_runner.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Layout/EmptyLines: - Exclude: - - 'app/controllers/tournaments_controller.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: around, only_before -Layout/EmptyLinesAroundAccessModifier: - Exclude: - - 'db/migrate/20220516203839_create_active_storage_variant_records.active_storage.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines, beginning_only, ending_only -Layout/EmptyLinesAroundClassBody: - Exclude: - - 'config/application.rb' - -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: IndentationWidth. -# SupportedStyles: special_inside_parentheses, consistent, align_brackets -Layout/FirstArrayElementIndentation: - EnforcedStyle: consistent - -# Offense count: 6 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, IndentationWidth. -# SupportedStyles: special_inside_parentheses, consistent, align_braces -Layout/FirstHashElementIndentation: - Exclude: - - 'spec/e2e/http/tournament_follow_sync_spec.rb' - -# Offense count: 5 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: normal, indented_internal_methods -Layout/IndentationConsistency: - Exclude: - - 'db/migrate/20220516203839_create_active_storage_variant_records.active_storage.rb' - - 'e2e/lib/scenario_runner.rb' - - 'spec/routing/tournaments_routing_spec.rb' - -# Offense count: 3 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Width, EnforcedStyleAlignWith, AllowedPatterns. -# SupportedStylesAlignWith: start_of_line, relative_to_receiver -Layout/IndentationWidth: - Exclude: - - 'db/migrate/20220516203839_create_active_storage_variant_records.active_storage.rb' - - 'spec/routing/tournaments_routing_spec.rb' - -# Offense count: 10 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, IndentationWidth. -# SupportedStyles: aligned, indented, indented_relative_to_receiver -Layout/MultilineMethodCallIndentation: - Exclude: - - 'app/interactors/advance_teams_in_intermediate_stage.rb' - - 'e2e/lib/scenario_runner.rb' - - 'spec/e2e/http/api_surface_spec.rb' - - 'spec/e2e/http/tournament_follow_sync_live_websocket_spec.rb' - - 'spec/e2e/http/tournament_follow_sync_spec.rb' - - 'spec/e2e/http/tournament_lifecycle_spec.rb' - - 'spec/e2e/http/tournament_live_websocket_spec.rb' - -# Offense count: 3 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, IndentationWidth. -# SupportedStyles: aligned, indented -Layout/MultilineOperationIndentation: - Exclude: - - 'app/services/group_stage_service.rb' - -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -Layout/SpaceInsidePercentLiteralDelimiters: - Exclude: - - 'db/migrate/20220516203839_create_active_storage_variant_records.active_storage.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: final_newline, final_blank_line -Layout/TrailingEmptyLines: - Exclude: - - 'spec/support/profile.rb' - -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AllowSafeAssignment. -Lint/AssignmentInCondition: - Exclude: - - 'db/migrate/20220516203838_add_service_name_to_active_storage_blobs.active_storage.rb' - -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -Lint/ScriptPermission: - Exclude: - - 'script/e2e_scenarios.rb' - - 'script/wait_for_http.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods, NotImplementedExceptions. -# NotImplementedExceptions: NotImplementedError -Lint/UnusedMethodArgument: - Exclude: - - 'app/controllers/application_controller.rb' - -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -Lint/UselessAssignment: - Exclude: - - 'e2e/lib/scenario_runner.rb' - - 'spec/models/beamer_spec.rb' - -# Offense count: 6 -# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes, Max. -Metrics/AbcSize: - Exclude: - - 'app/controllers/tournaments_controller.rb' - - 'app/services/tournament_snapshot_importer.rb' - - 'e2e/lib/scenario_runner.rb' - - 'spec/e2e/http/tournament_follow_sync_spec.rb' - - 'spec/services/tournament_snapshot_importer_spec.rb' - -# Offense count: 2 -# Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns. -# AllowedMethods: refine -Metrics/BlockLength: - Exclude: - - 'app/services/tournament_snapshot_importer.rb' - - 'db/schema.rb' - -# Offense count: 15 -# Configuration parameters: AllowedMethods, AllowedPatterns, Max. -Metrics/CyclomaticComplexity: - Exclude: - - 'app/controllers/matches_controller.rb' - - 'app/controllers/tournaments_controller.rb' - - 'app/models/group_score.rb' - - 'app/models/stage.rb' - - 'app/services/group_stage_service.rb' - - 'app/services/playoff_stage_service.rb' - - 'e2e/lib/api_client.rb' - - 'spec/e2e/http/tournament_follow_sync_spec.rb' - - 'spec/services/tournament_snapshot_importer_spec.rb' - -# Offense count: 1 -# Configuration parameters: CountComments, Max, CountAsOne, AllowedMethods, AllowedPatterns. -Metrics/MethodLength: - Exclude: - - 'app/controllers/tournaments_controller.rb' - -# Offense count: 1 -# Configuration parameters: Max, CountKeywordArgs, MaxOptionalParameters. -Metrics/ParameterLists: - Exclude: - - 'e2e/lib/api_client.rb' - -# Offense count: 14 -# Configuration parameters: AllowedMethods, AllowedPatterns, Max. -Metrics/PerceivedComplexity: - Exclude: - - 'app/controllers/matches_controller.rb' - - 'app/controllers/tournaments_controller.rb' - - 'app/models/group_score.rb' - - 'app/models/stage.rb' - - 'app/services/group_stage_service.rb' - - 'e2e/lib/api_client.rb' - - 'spec/e2e/http/tournament_follow_sync_spec.rb' - - 'spec/services/tournament_snapshot_importer_spec.rb' - -# Offense count: 3 -# This cop supports unsafe autocorrection (--autocorrect-all). -Performance/Count: - Exclude: - - 'app/controllers/tournaments_controller.rb' - - 'spec/services/tournament_snapshot_importer_spec.rb' - -# Offense count: 2 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: SafeMultiline. -Performance/DeleteSuffix: - Exclude: - - 'e2e/lib/action_cable_client.rb' - - 'e2e/lib/api_client.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Performance/RegexpMatch: - Exclude: - - 'config/spring.rb' - -# Offense count: 14 -# This cop supports unsafe autocorrection (--autocorrect-all). -Performance/TimesMap: - Exclude: - - 'e2e/lib/scenario_runner.rb' - - 'spec/e2e/http/api_surface_spec.rb' - - 'spec/e2e/http/tournament_beamers_websocket_spec.rb' - - 'spec/e2e/http/tournament_follow_sync_beamers_websocket_spec.rb' - - 'spec/e2e/http/tournament_follow_sync_spec.rb' - - 'spec/e2e/http/tournament_follow_sync_websocket_spec.rb' - - 'spec/e2e/http/tournament_sync_test_spec.rb' - - 'spec/e2e/http/tournament_team_action_lists_websocket_spec.rb' - - 'spec/e2e/http/tournament_timer_follow_sync_websocket_spec.rb' - - 'spec/e2e/http/tournament_timer_websocket_spec.rb' - - 'spec/services/tournament_snapshot_importer_spec.rb' - -# Offense count: 3 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: NilOrEmpty, NotPresent, UnlessPresent. -Rails/Blank: - Exclude: - - 'e2e/lib/scenario_runner.rb' - - 'spec/e2e/http/tournament_sync_test_spec.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforceForPrefixed. -Rails/Delegate: - Exclude: - - 'e2e/lib/action_cable_client.rb' - -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: slashes, arguments -Rails/FilePath: - Exclude: - - 'config/environments/development.rb' - - 'spec/rails_helper.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Rails/IndexBy: - Exclude: - - 'spec/e2e/http/tournament_follow_sync_spec.rb' - -# Offense count: 1 -# Configuration parameters: IgnoreScopes. -Rails/InverseOf: - Exclude: - - 'app/models/tournament.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: NotNilAndNotEmpty, NotBlank, UnlessBlank. -Rails/Present: - Exclude: - - 'e2e/lib/api_client.rb' - -# Offense count: 1 -Rails/ReversibleMigration: - Exclude: - - 'db/migrate/20240407180900_add_default_to_position_in_group_scores.rb' - -# Offense count: 9 -# Configuration parameters: ForbiddenMethods, AllowedMethods. -# ForbiddenMethods: decrement!, decrement_counter, increment!, increment_counter, insert, insert!, insert_all, insert_all!, toggle!, touch, touch_all, update_all, update_attribute, update_column, update_columns, update_counters, upsert, upsert_all -Rails/SkipsModelValidations: - Exclude: - - 'app/models/tournament_sync_queue_entry.rb' - - 'app/services/tournament_sync_pusher.rb' - - 'db/migrate/20220516203838_add_service_name_to_active_storage_blobs.active_storage.rb' - - 'db/migrate/20260416102000_backfill_default_beamers_by_name.rb' - - 'spec/controllers/tournaments_controller_spec.rb' - -# Offense count: 33 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: strict, flexible -Rails/TimeZone: - Exclude: - - 'e2e/lib/action_cable_client.rb' - - 'e2e/lib/scenario_runner.rb' - - 'spec/e2e/http/tournament_beamers_websocket_spec.rb' - - 'spec/e2e/http/tournament_follow_sync_beamers_websocket_spec.rb' - - 'spec/e2e/http/tournament_follow_sync_spec.rb' - - 'spec/e2e/http/tournament_follow_sync_websocket_spec.rb' - - 'spec/e2e/http/tournament_team_action_lists_websocket_spec.rb' - - 'spec/e2e/http/tournament_timer_follow_sync_websocket_spec.rb' - - 'spec/e2e/http/tournament_timer_websocket_spec.rb' - -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, ProceduralMethods, FunctionalMethods, AllowedMethods, AllowedPatterns, AllowBracesOnProceduralOneLiners, BracesRequiredMethods. -# SupportedStyles: line_count_based, semantic, braces_for_chaining, always_braces -# ProceduralMethods: benchmark, bm, bmbm, create, each_with_object, measure, new, realtime, tap, with_object -# FunctionalMethods: let, let!, subject, watch -# AllowedMethods: lambda, proc, it -Style/BlockDelimiters: - Exclude: - - 'spec/controllers/beamers_controller_spec.rb' - -# Offense count: 2 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Keywords, RequireColon. -# Keywords: TODO, FIXME, OPTIMIZE, HACK, REVIEW, NOTE -Style/CommentAnnotation: - Exclude: - - 'spec/controllers/matches_controller_spec.rb' - - 'spec/services/group_stage_service_spec.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions. -# SupportedStyles: assign_to_condition, assign_inside_condition -Style/ConditionalAssignment: - Exclude: - - 'spec/factories/tournaments.rb' - -# Offense count: 12 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyle. -# SupportedStyles: always, always_true, never -Style/FrozenStringLiteralComment: - Exclude: - - 'config/boot.rb' - - 'config/initializers/filter_parameter_logging.rb' - - 'config/initializers/new_framework_defaults_7_0.rb' - - 'db/migrate/20220516203838_add_service_name_to_active_storage_blobs.active_storage.rb' - - 'db/migrate/20220516203839_create_active_storage_variant_records.active_storage.rb' - - 'db/migrate/20220516203840_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb' - - 'db/migrate/20240407175005_add_position_to_group_scores.rb' - - 'db/migrate/20240407180900_add_default_to_position_in_group_scores.rb' - - 'db/migrate/20250309202658_add_timer_end_to_tournaments.rb' - - 'db/migrate/20260423133000_add_timer_mode_to_tournaments.rb' - - 'db/schema.rb' - - 'spec/support/profile.rb' - -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/GlobalStdStream: - Exclude: - - 'config/environments/production.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: MinBodyLength, AllowConsecutiveConditionals. -Style/GuardClause: - Exclude: - - 'db/migrate/20220516203838_add_service_name_to_active_storage_blobs.active_storage.rb' - -# Offense count: 2 -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/IdenticalConditionalBranches: - Exclude: - - 'spec/e2e/http/tournament_lifecycle_spec.rb' - -# Offense count: 18 -# This cop supports safe autocorrection (--autocorrect). -Style/IfUnlessModifier: - Exclude: - - 'app/controllers/tournaments_controller.rb' - - 'app/services/group_stage_service.rb' - - 'app/services/playoff_stage_service.rb' - - 'e2e/lib/scenario_runner.rb' - -# Offense count: 2 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: InverseMethods, InverseBlocks. -Style/InverseMethods: - Exclude: - - 'spec/e2e/http/tournament_lifecycle_spec.rb' - -# Offense count: 3 -Style/MultilineBlockChain: - Exclude: - - 'e2e/lib/api_client.rb' - - 'spec/e2e/http/tournament_follow_sync_spec.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: MinDigits, Strict, AllowedNumbers, AllowedPatterns. -Style/NumericLiterals: - Exclude: - - 'db/schema.rb' - -# Offense count: 2 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: EnforcedStyle, AllowedMethods, AllowedPatterns. -# SupportedStyles: predicate, comparison -Style/NumericPredicate: - Exclude: - - 'e2e/lib/scenario_runner.rb' - -# Offense count: 1 -# Configuration parameters: AllowedMethods. -# AllowedMethods: respond_to_missing? -Style/OptionalBooleanParameter: - Exclude: - - 'app/services/playoff_stage_service.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -Style/RedundantFreeze: - Exclude: - - 'e2e/lib/scenario_runner.rb' - -# Offense count: 3 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: AllowMultipleReturnValues. -Style/RedundantReturn: - Exclude: - - 'app/controllers/tournaments_controller.rb' - -# Offense count: 3 -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/RedundantSort: - Exclude: - - 'spec/e2e/http/tournament_follow_sync_spec.rb' - -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods, MaxChainLength. -# AllowedMethods: present?, blank?, presence, try, try! -Style/SafeNavigation: - Exclude: - - 'config/spring.rb' - -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: Mode. -Style/StringConcatenation: - Exclude: - - 'config/spring.rb' - -# Offense count: 246 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline. -# SupportedStyles: single_quotes, double_quotes -Style/StringLiterals: - Exclude: - - 'config/boot.rb' - - 'db/migrate/20220516203839_create_active_storage_variant_records.active_storage.rb' - - 'db/schema.rb' - - 'spec/e2e/http/api_surface_spec.rb' - -# Offense count: 3 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: . -# SupportedStyles: percent, brackets -Style/SymbolArray: - EnforcedStyle: percent - MinSize: 10 - -# Offense count: 1 -# This cop supports unsafe autocorrection (--autocorrect-all). -# Configuration parameters: AllowMethodsWithArguments, AllowedMethods, AllowedPatterns, AllowComments. -# AllowedMethods: define_method, mail, respond_to -Style/SymbolProc: - Exclude: - - 'spec/services/tournament_snapshot_importer_spec.rb' - -# Offense count: 1 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: EnforcedStyleForMultiline. -# SupportedStylesForMultiline: comma, consistent_comma, diff_comma, no_comma -Style/TrailingCommaInArrayLiteral: - Exclude: - - 'spec/services/playoff_stage_service_spec.rb' - -# Offense count: 3 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: WordRegex. -# SupportedStyles: percent, brackets -Style/WordArray: - EnforcedStyle: percent - MinSize: 3 - -# Offense count: 6 -# This cop supports unsafe autocorrection (--autocorrect-all). -Style/ZeroLengthPredicate: - Exclude: - - 'app/models/stage.rb' - - 'app/services/group_stage_service.rb' - -# Offense count: 113 -# This cop supports safe autocorrection (--autocorrect). -# Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings. -# URISchemes: http, https -Layout/LineLength: - Exclude: - - 'app/controllers/matches_controller.rb' - - 'app/controllers/tournaments_controller.rb' - - 'app/models/team_action_list.rb' - - 'app/services/group_stage_service.rb' - - 'app/services/playoff_stage_service.rb' - - 'db/schema.rb' - - 'e2e/lib/api_client.rb' - - 'e2e/lib/scenario_runner.rb' - - 'script/e2e_scenarios.rb' - - 'spec/controllers/groups_controller_spec.rb' - - 'spec/controllers/matches_controller_spec.rb' - - 'spec/controllers/stages_controller_spec.rb' - - 'spec/controllers/team_action_lists_controller_spec.rb' - - 'spec/e2e/http/api_surface_spec.rb' - - 'spec/e2e/http/tournament_follow_sync_spec.rb' - - 'spec/e2e/http/tournament_follow_sync_websocket_spec.rb' - - 'spec/e2e/http/tournament_lifecycle_spec.rb' - - 'spec/e2e/http/tournament_rendering_spec.rb' - - 'spec/e2e/http/tournament_sync_test_spec.rb' - - 'spec/e2e/http/tournament_team_action_lists_websocket_spec.rb' - - 'spec/e2e/http/tournament_timer_follow_sync_websocket_spec.rb' - - 'spec/factories/tournaments.rb' - - 'spec/services/group_stage_service_spec.rb' - - 'spec/services/team_action_list_service_spec.rb' - - 'spec/services/tournament_snapshot_importer_spec.rb' - - 'spec/services/tournament_sync_processor_spec.rb' diff --git a/Dockerfile b/Dockerfile index e7c10d1..ec73507 100644 --- a/Dockerfile +++ b/Dockerfile @@ -111,6 +111,7 @@ COPY script /app/script COPY config.ru Rakefile /app/ COPY config/environments/production.rb /app/config/environments/beta.rb COPY tasks.py /app/tasks.py +COPY .rubocop.yml /app/.rubocop.yml COPY e2e /app/e2e COPY lib /app/lib COPY spec /app/spec diff --git a/Gemfile b/Gemfile index 29fdec4..7fdce7e 100644 --- a/Gemfile +++ b/Gemfile @@ -44,8 +44,8 @@ group :test, optional: true do gem 'factory_bot_rails' gem 'faker' gem 'rspec-rails' - gem 'shoulda-matchers' gem 'ruby-prof' + gem 'shoulda-matchers' end group :development, optional: true do @@ -53,13 +53,13 @@ group :development, optional: true do # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring gem 'railroady' # Use sqlite3 as the database for Active Record - gem 'sqlite3' gem 'rubocop' gem 'rubocop-performance' gem 'rubocop-rails' gem 'solargraph' gem 'spring' gem 'spring-watcher-listen', '~> 2.1' + gem 'sqlite3' end group :production do diff --git a/app/controllers/matches_controller.rb b/app/controllers/matches_controller.rb index 2ea5ac3..17d8a82 100644 --- a/app/controllers/matches_controller.rb +++ b/app/controllers/matches_controller.rb @@ -38,7 +38,8 @@ class MatchesController < ApplicationController elsif match.save UpdateGroupsGroupScoresAndSave.call(group: @group) group_scores = group_scores_for_log(@group) - TournamentTransactionLogger.record_decider_match_created!(match: match, user: current_user, group_scores: group_scores) + TournamentTransactionLogger.record_decider_match_created!(match: match, user: current_user, + group_scores: group_scores) synced_tournament = @group.tournament render json: match, include: %w[match_scores.team group], status: :created else @@ -90,7 +91,8 @@ class MatchesController < ApplicationController # PATCH /matches/1/swap def swap unless @match.swappable_with?(@swap_target) - render json: { error: 'Matches must be different group matches from the same group' }, status: :unprocessable_content + render json: { error: 'Matches must be different group matches from the same group' }, + status: :unprocessable_content return end @@ -107,7 +109,8 @@ class MatchesController < ApplicationController end swapped_matches = Match.where(id: [@match.id, @swap_target.id]).order(:position) - render json: swapped_matches, each_serializer: ExtendedMatchSerializer, include: %w[match_scores.team bets stage group] + render json: swapped_matches, each_serializer: ExtendedMatchSerializer, + include: %w[match_scores.team bets stage group] push_sync_if_needed!(synced_tournament) TournamentLiveBroadcast.call(synced_tournament.id) end @@ -117,9 +120,7 @@ class MatchesController < ApplicationController def handle_match_end if @match.group_match? group = @match.group - unless UpdateGroupsGroupScoresAndSave.call(group: group).success? - logger.warn "Updating groups group score failed for #{group}" - end + logger.warn "Updating groups group score failed for #{group}" unless UpdateGroupsGroupScoresAndSave.call(group: group).success? @transaction_log_group_scores = group_scores_for_log(group) return end @@ -290,9 +291,7 @@ class MatchesController < ApplicationController decider: true, match_scores: teams.map { |team| MatchScore.new(team: team) } ).tap do |match| - if team_ids.size != 2 || teams.size != 2 - match.errors.add(:team_ids, 'must contain exactly two teams from this group') - end + match.errors.add(:team_ids, 'must contain exactly two teams from this group') if team_ids.size != 2 || teams.size != 2 end end diff --git a/app/controllers/teams_controller.rb b/app/controllers/teams_controller.rb index 0450240..d6e2808 100644 --- a/app/controllers/teams_controller.rb +++ b/app/controllers/teams_controller.rb @@ -71,9 +71,7 @@ class TeamsController < ApplicationController return [] if id_values.empty? User.where(id: id_values).tap do |players| - unless players.size == id_values.uniq.size - raise ActiveRecord::RecordNotFound.new('Could not find all players', 'User') - end + raise ActiveRecord::RecordNotFound.new('Could not find all players', 'User') unless players.size == id_values.uniq.size end end diff --git a/app/controllers/tournaments_controller.rb b/app/controllers/tournaments_controller.rb index 04f92c2..8bbebcf 100644 --- a/app/controllers/tournaments_controller.rb +++ b/app/controllers/tournaments_controller.rb @@ -14,7 +14,8 @@ class TournamentsController < ApplicationController before_action :set_tournament_for_show, only: %i[show] before_action :set_tournament, only: %i[update destroy destroy_playoffs set_timer timer sync_state test_sync start_next_group_matches] - before_action :authenticate_user!, only: %i[create update destroy destroy_playoffs set_timer test_sync start_next_group_matches] + before_action :authenticate_user!, + only: %i[create update destroy destroy_playoffs set_timer test_sync start_next_group_matches] before_action -> { require_owner! @tournament.owner }, only: %i[update destroy destroy_playoffs set_timer test_sync start_next_group_matches] before_action :validate_create_params, only: %i[create] @@ -74,6 +75,7 @@ class TournamentsController < ApplicationController end raise ActionController::ParameterMissing, :teams if teams.nil? + if group_stage params.require(:playoff_teams_amount) groups = organize_teams_in_groups(teams) @@ -339,7 +341,7 @@ class TournamentsController < ApplicationController return if ActiveModel::Type::Boolean.new.cast(params[:read_only_mode]) teams = params['teams'] - return if teams.is_a?(Array) && teams.reject { |t| t.is_a? ActionController::Parameters }.count.zero? + return if teams.is_a?(Array) && teams.count { |team| !team.is_a?(ActionController::Parameters) }.zero? render json: { error: 'Invalid teams array' }, status: :unprocessable_content end @@ -374,12 +376,16 @@ class TournamentsController < ApplicationController error: 'playoff_teams_amount, instant_finalists_amount and intermediate_round_participants_amount don\'t match' }, status: :unprocessable_content end + def sync_state_params params.require(:snapshot).permit!.to_h end def authorize_sync_request! - return render json: { error: 'Tournament is not configured to accept sync pushes' }, status: :unprocessable_content unless @tournament.sync_accepts_push? + unless @tournament.sync_accepts_push? + return render json: { error: 'Tournament is not configured to accept sync pushes' }, + status: :unprocessable_content + end token = request.authorization.to_s.delete_prefix('Bearer ').presence || request.headers['X-Tournament-Sync-Token'].to_s return render json: { error: 'Missing sync token' }, status: :unauthorized if token.blank? @@ -425,13 +431,9 @@ class TournamentsController < ApplicationController timer_reason_text = params[:timer_reason_text] clear_timer = ActiveModel::Type::Boolean.new.cast(params[:clear_timer]) - if clear_timer - return validate_clear_timer_params(timestamp, timestamp_seconds, timer_mode, timer_reason, timer_reason_text) - end + return validate_clear_timer_params(timestamp, timestamp_seconds, timer_mode, timer_reason, timer_reason_text) if clear_timer - if timer_mode.present? && !Tournament::TIMER_MODES.include?(timer_mode) - return render_timer_validation_error('Invalid timer_mode') - end + return render_timer_validation_error('Invalid timer_mode') if timer_mode.present? && !Tournament::TIMER_MODES.include?(timer_mode) timer_reason_error = validate_timer_reason_params( timer_reason, @@ -441,7 +443,20 @@ class TournamentsController < ApplicationController ) return render_timer_validation_error(timer_reason_error) if timer_reason_error - # throw error if both timestamp and timestamp_seconds are present + return if validate_timer_timestamp_params(timestamp, timestamp_seconds, timer_mode) + + if timer_mode == 'countup' + params[:timestamp] = Time.zone.now + elsif timer_mode.present? + render_timer_validation_error('timer_mode requires timestamp, timestamp_seconds, or clear_timer') + elsif timer_reason_update_requested + render_timer_validation_error('timer_reason requires active timer') unless timer_active? + else + render_timer_validation_error('timestamp, timestamp_seconds, or clear_timer is required') + end + end + + def validate_timer_timestamp_params(timestamp, timestamp_seconds, timer_mode) if timestamp.present? && timestamp_seconds.present? return render_timer_validation_error('Only one of timestamp or timestamp_seconds is allowed') end @@ -450,43 +465,34 @@ class TournamentsController < ApplicationController return render_timer_validation_error('timestamp_seconds is only allowed for countdown mode') end - if timestamp_seconds.present? - begin - timestamp_seconds = Integer(timestamp_seconds) - rescue ArgumentError - return render_timer_validation_error('Invalid seconds format') - end + return validate_timestamp_seconds_param(timestamp_seconds) if timestamp_seconds.present? + return validate_timestamp_param(timestamp, timer_mode) if timestamp.present? - if timestamp_seconds <= 0 - return render_timer_validation_error('Timestamp must be in the future for countdown mode') - end + false + end - parsed_time = Time.zone.now + timestamp_seconds - params[:timestamp] = parsed_time - params[:timer_mode] = 'countdown' - elsif timestamp.present? - begin - parsed_time = Time.zone.parse(timestamp) - if parsed_time.nil? - return render_timer_validation_error('Invalid timestamp format') - elsif timer_mode != 'countup' && !parsed_time.future? - return render_timer_validation_error('Timestamp must be in the future for countdown mode') - end + def validate_timestamp_seconds_param(timestamp_seconds) + seconds = Integer(timestamp_seconds) + return render_timer_validation_error('Timestamp must be in the future for countdown mode') if seconds <= 0 - params[:timestamp] = parsed_time - params[:timer_mode] = timer_mode.presence || 'countdown' - rescue ArgumentError - return render_timer_validation_error('Invalid timestamp format') - end - elsif timer_mode == 'countup' - params[:timestamp] = Time.zone.now - elsif timer_mode.present? - return render_timer_validation_error('timer_mode requires timestamp, timestamp_seconds, or clear_timer') - elsif timer_reason_update_requested - return render_timer_validation_error('timer_reason requires active timer') unless timer_active? - else - return render_timer_validation_error('timestamp, timestamp_seconds, or clear_timer is required') + params[:timestamp] = Time.zone.now + seconds + params[:timer_mode] = 'countdown' + rescue ArgumentError + render_timer_validation_error('Invalid seconds format') + end + + def validate_timestamp_param(timestamp, timer_mode) + parsed_time = Time.zone.parse(timestamp) + if parsed_time.nil? + return render_timer_validation_error('Invalid timestamp format') + elsif timer_mode != 'countup' && !parsed_time.future? + return render_timer_validation_error('Timestamp must be in the future for countdown mode') end + + params[:timestamp] = parsed_time + params[:timer_mode] = timer_mode.presence || 'countdown' + rescue ArgumentError + render_timer_validation_error('Invalid timestamp format') end def normalize_timer_reason_params @@ -518,17 +524,13 @@ class TournamentsController < ApplicationController timer_reason_param_present, timer_reason_text_param_present ) - if timer_reason_text_param_present && !timer_reason_param_present - return 'timer_reason is required when timer_reason_text is provided' - end + return 'timer_reason is required when timer_reason_text is provided' if timer_reason_text_param_present && !timer_reason_param_present return if timer_reason.blank? && timer_reason_text.blank? return 'timer_reason_text is only allowed when timer_reason is custom' if timer_reason.blank? return 'Invalid timer_reason' unless Tournament::TIMER_REASONS.include?(timer_reason) - if timer_reason == 'custom' && timer_reason_text.blank? - return 'timer_reason_text is required for custom timer_reason' - end + return 'timer_reason_text is required for custom timer_reason' if timer_reason == 'custom' && timer_reason_text.blank? return unless timer_reason != 'custom' && timer_reason_text.present? diff --git a/app/interactors/advance_teams_in_intermediate_stage.rb b/app/interactors/advance_teams_in_intermediate_stage.rb index 18db320..18f2cb5 100644 --- a/app/interactors/advance_teams_in_intermediate_stage.rb +++ b/app/interactors/advance_teams_in_intermediate_stage.rb @@ -16,9 +16,9 @@ class AdvanceTeamsInIntermediateStage # populate stage below with the "winners" from single team matches matches.select { |m| m.state == 'single_team' } - .each do |match| - context.fail! unless PopulateMatchBelowAndSave.call(match: match).success? - end + .each do |match| + context.fail! unless PopulateMatchBelowAndSave.call(match:).success? + end (context.object_to_save ||= []) << intermediate_stage end end diff --git a/app/models/stage.rb b/app/models/stage.rb index fd78498..3784d18 100644 --- a/app/models/stage.rb +++ b/app/models/stage.rb @@ -10,9 +10,9 @@ class Stage < ApplicationRecord delegate :owner, to: :tournament def teams - if !matches.size.zero? + if !matches.empty? matches.map(&:teams).flatten.uniq - elsif !groups.size.zero? + elsif !groups.empty? groups.map(&:teams).flatten.uniq else [] @@ -20,9 +20,9 @@ class Stage < ApplicationRecord end def over? - return matches.find { |m| m.state != 'finished' }.nil? unless matches.size.zero? + return matches.find { |m| m.state != 'finished' }.nil? unless matches.empty? - unless groups.size.zero? && groups.map(&:matches).flatten.size.zero? + unless groups.empty? && groups.map(&:matches).flatten.empty? relevant_matches = groups.map(&:matches).flatten.reject(&:decider?) return relevant_matches.find { |m| m.state != 'finished' }.nil? end diff --git a/app/models/team_action_list.rb b/app/models/team_action_list.rb index 018bd0a..e94ea20 100644 --- a/app/models/team_action_list.rb +++ b/app/models/team_action_list.rb @@ -6,7 +6,8 @@ class TeamActionList < ApplicationRecord validates :name, :key, :action_name, :source, presence: true validates :key, uniqueness: { scope: :tournament_id } - validates :key, format: { with: /\A[a-z0-9_]+\z/, message: 'may only contain lowercase letters, numbers, and underscores' } + validates :key, + format: { with: /\A[a-z0-9_]+\z/, message: 'may only contain lowercase letters, numbers, and underscores' } delegate :owner, to: :tournament @@ -20,7 +21,7 @@ class TeamActionList < ApplicationRecord team_action_items.where.not(team_id: desired_team_ids).destroy_all desired_team_ids.each do |team_id| - team_action_items.find_or_create_by!(team_id: team_id) + team_action_items.find_or_create_by!(team_id:) end end end diff --git a/app/models/tournament.rb b/app/models/tournament.rb index 6f0ad3a..dee1591 100644 --- a/app/models/tournament.rb +++ b/app/models/tournament.rb @@ -9,7 +9,7 @@ class Tournament < ApplicationRecord METADATA_STRING_ATTRIBUTES = %i[location website_url].freeze belongs_to :user - belongs_to :owner, class_name: 'User', foreign_key: :user_id + belongs_to :owner, class_name: 'User', foreign_key: :user_id, inverse_of: :tournaments has_many :teams, dependent: :destroy has_many :stages, dependent: :destroy has_many :beamers, dependent: :destroy diff --git a/app/models/tournament_sync_queue_entry.rb b/app/models/tournament_sync_queue_entry.rb index 297cd4b..e1fcc19 100644 --- a/app/models/tournament_sync_queue_entry.rb +++ b/app/models/tournament_sync_queue_entry.rb @@ -45,9 +45,9 @@ class TournamentSyncQueueEntry < ApplicationRecord def acquire_lock! token = SecureRandom.hex(8) updated = self.class - .where(id: id) + .where(id:) .where(locked_at: nil) - .or(self.class.where(id: id).where('locked_at < ?', Time.current - LOCK_TTL)) + .or(self.class.where(id:).where('locked_at < ?', Time.current - LOCK_TTL)) .update_all(locked_at: Time.current, lock_token: token) return nil if updated.zero? diff --git a/app/serializers/tournament_serializer.rb b/app/serializers/tournament_serializer.rb index f23cf83..30c4503 100644 --- a/app/serializers/tournament_serializer.rb +++ b/app/serializers/tournament_serializer.rb @@ -7,7 +7,7 @@ class TournamentSerializer < SimpleTournamentSerializer attribute :sync_target_url, if: :sync_metadata_visible? attribute :sync_auth_configured, if: :sync_metadata_visible? attribute :sync_last_push_error, if: :sync_metadata_visible? - # NEVER expose sync_auth_token anywhere - it should only ever be written to or checked against + # NEVER expose sync_auth_token anywhere - it should only ever be written to or checked against has_many :stages has_many :team_action_lists diff --git a/app/services/group_stage_service.rb b/app/services/group_stage_service.rb index c4ac9e8..c3855b3 100644 --- a/app/services/group_stage_service.rb +++ b/app/services/group_stage_service.rb @@ -3,18 +3,18 @@ class GroupStageService class << self def generate_group_stage(groups) - raise 'Cannot generate group stage without groups' if groups.length.zero? + raise 'Cannot generate group stage without groups' if groups.empty? # raise an error if the average group size is not a whole number raise 'Groups need to be equal size' unless (groups.flatten.length.to_f / groups.length % 1).zero? groups = groups.map(&method(:get_group_object_from)).each_with_index { |group, i| group.number = i + 1 } - Stage.new level: -1, groups: groups, state: :in_progress + Stage.new level: -1, groups:, state: :in_progress end def get_group_object_from(team_array) Group.new matches: generate_all_matches_between(team_array), - group_scores: team_array.map { |team| GroupScore.new team: team } + group_scores: team_array.map { |team| GroupScore.new team: } end def deal_with_spacing_of_teams(matches, team_size) @@ -71,7 +71,7 @@ class GroupStageService def update_group_scores(group) changed_group_scores = [] group.teams.each do |team| - group_score = group.group_scores.find_by(team: team) + group_score = group.group_scores.find_by(team:) matches = group.matches.select { |match| match.teams.include? team } # reset previous values group_score.group_points = 0 @@ -217,7 +217,7 @@ class GroupStageService def comparison_partner_index(sorted_group_scores, index) return index - 1 if index.positive? && sorted_group_scores[index - 1].position == sorted_group_scores[index].position return index + 1 if index < sorted_group_scores.size - 1 && - sorted_group_scores[index + 1].position == sorted_group_scores[index].position + sorted_group_scores[index + 1].position == sorted_group_scores[index].position return index - 1 if index.positive? return index + 1 if sorted_group_scores.size > 1 @@ -239,11 +239,9 @@ class GroupStageService start_index -= 1 while start_index.positive? && sorted_group_scores[start_index - 1].position == position end_index = index - while end_index < sorted_group_scores.size - 1 && sorted_group_scores[end_index + 1].position == position - end_index += 1 - end + end_index += 1 while end_index < sorted_group_scores.size - 1 && sorted_group_scores[end_index + 1].position == position - { start_index: start_index, end_index: end_index } + { start_index:, end_index: } end def advancing_slots_for_group(group) @@ -272,7 +270,7 @@ class GroupStageService advancing_slots = advancing_slots_for_group(group) group_ready_for_decider_check = regular_group_matches_finished?(group) - cache = sorted_group_scores.each_with_index.each_with_object({}) do |(group_score, index), decisions| + cache = sorted_group_scores.each_with_index.with_object({}) do |(group_score, index), decisions| next if sorted_group_scores.size <= 1 comparison_index = comparison_partner_index(sorted_group_scores, index) @@ -296,8 +294,9 @@ class GroupStageService decider_match_id: comparison_reason[:decider_match_id], tied: comparison_reason[:tied], needs_decider_match: group_ready_for_decider_check && - comparison_reason[:tied] && - unresolved_tie_affects_advancing_placements?(sorted_group_scores, group_score, advancing_slots) + comparison_reason[:tied] && + unresolved_tie_affects_advancing_placements?(sorted_group_scores, group_score, + advancing_slots) } end diff --git a/app/services/playoff_stage_service.rb b/app/services/playoff_stage_service.rb index 20def6b..085c30c 100644 --- a/app/services/playoff_stage_service.rb +++ b/app/services/playoff_stage_service.rb @@ -16,7 +16,7 @@ class PlayoffStageService initial_stage.state = :intermediate_stage unless initial_stage.matches.find(&:single_team?).nil? playoffs << initial_stage # empty stages are the stages, the tournament is filled with to have the matches ready for later - empty_stages = generate_stages_with_empty_matches(stage_count - 1, teams.size > 3) + empty_stages = generate_stages_with_empty_matches(stage_count - 1, include_third_place_match: teams.size > 3) playoffs.concat empty_stages playoffs end @@ -25,7 +25,7 @@ class PlayoffStageService # # @param stage_count [Integer] number of stages to generate # @return [Array] the generated stages - def generate_stages_with_empty_matches(stage_count, include_third_place_match = false) + def generate_stages_with_empty_matches(stage_count, include_third_place_match: false) empty_stages = [] stage_count.times do |i| match_count = 2**i @@ -82,7 +82,9 @@ class PlayoffStageService # of which the winners advance into the match below # depending on the position of the match, the companion match is either on the left or right of it companion_match = find_companion_match(current_match.position, current_stage) - return populate_final_stage_matches(current_match, companion_match, next_stage) if semifinal_match?(current_stage, next_stage) + return populate_final_stage_matches(current_match, companion_match, next_stage) if semifinal_match?( + current_stage, next_stage + ) winners = get_winners_of(companion_match, current_match) populate_match!(match: next_stage.matches.find { |m| m.position == current_match.position / 2 }, teams: winners) diff --git a/app/services/team_action_item_update.rb b/app/services/team_action_item_update.rb index 955051e..8775d19 100644 --- a/app/services/team_action_item_update.rb +++ b/app/services/team_action_item_update.rb @@ -3,7 +3,7 @@ class TeamActionItemUpdate class << self def call(team_action_item:, completed:) - new(team_action_item: team_action_item, completed: completed).call + new(team_action_item:, completed:).call end end @@ -13,7 +13,7 @@ class TeamActionItemUpdate end def call - return false unless team_action_item.update(completed: completed) + return false unless team_action_item.update(completed:) TournamentSyncEnqueue.call(team_action_item.tournament) true diff --git a/app/services/tournament_playoff_deletion.rb b/app/services/tournament_playoff_deletion.rb index fbeafd8..0e3066b 100644 --- a/app/services/tournament_playoff_deletion.rb +++ b/app/services/tournament_playoff_deletion.rb @@ -23,9 +23,7 @@ class TournamentPlayoffDeletion playoff_stages = tournament.stages.reload.select { |stage| stage.level >= 0 } return failure('Playoffs have not been generated') if playoff_stages.empty? - unless playoffs_deletable?(playoff_stages) - return failure('Playoffs cannot be deleted after matches have started or scores changed') - end + return failure('Playoffs cannot be deleted after matches have started or scores changed') unless playoffs_deletable?(playoff_stages) playoff_stages.each(&:destroy!) group_stage.update!(state: :in_progress) diff --git a/app/services/tournament_snapshot_importer.rb b/app/services/tournament_snapshot_importer.rb index 6582d27..898d23a 100644 --- a/app/services/tournament_snapshot_importer.rb +++ b/app/services/tournament_snapshot_importer.rb @@ -14,7 +14,7 @@ class TournamentSnapshotImporter ].freeze def self.import!(tournament:, snapshot:) - new(tournament: tournament, snapshot: snapshot).import! + new(tournament:, snapshot:).import! end def initialize(tournament:, snapshot:) @@ -222,6 +222,7 @@ class TournamentSnapshotImporter stale_records = existing_records.reject { |record| source_ids.include?(record.sync_source_id) } stale_records.each(&:destroy!) end + def parse_time(value) return nil if value.blank? @@ -242,6 +243,6 @@ class TournamentSnapshotImporter def code_taken_by_another_tournament?(code) return false if code.blank? - Tournament.where(code: code).where.not(id: tournament.id).exists? + Tournament.where(code:).where.not(id: tournament.id).exists? end end diff --git a/app/services/tournament_sync_enqueue.rb b/app/services/tournament_sync_enqueue.rb index f2694b7..3c28275 100644 --- a/app/services/tournament_sync_enqueue.rb +++ b/app/services/tournament_sync_enqueue.rb @@ -13,7 +13,7 @@ class TournamentSyncEnqueue return false unless tournament.sync_push_enabled? payload = TournamentSnapshotBuilder.build(fresh_tournament) - entry = TournamentSyncQueueEntry.find_or_initialize_by(tournament: tournament) + entry = TournamentSyncQueueEntry.find_or_initialize_by(tournament:) entry.snapshot_json = JSON.dump(payload) entry.status = 'pending' entry.attempts = 0 diff --git a/app/services/tournament_sync_pusher.rb b/app/services/tournament_sync_pusher.rb index bcb02b1..05f2c04 100644 --- a/app/services/tournament_sync_pusher.rb +++ b/app/services/tournament_sync_pusher.rb @@ -27,11 +27,9 @@ class TournamentSyncPusher end def push_snapshot!(snapshot) - response = perform_request(snapshot: snapshot) + response = perform_request(snapshot:) - unless response.is_a?(Net::HTTPSuccess) - raise SyncFailed, "sync push failed with status #{response.code}: #{response.body}" - end + raise SyncFailed, "sync push failed with status #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess) tournament.update_columns(sync_last_pushed_at: Time.current, sync_last_push_error: nil) true @@ -53,7 +51,7 @@ class TournamentSyncPusher request['accept'] = 'application/json' request['content-type'] = 'application/json' request['authorization'] = "Bearer #{tournament.sync_auth_token}" - request.body = JSON.dump({ snapshot: snapshot }) + request.body = JSON.dump({ snapshot: }) Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http| timeout = http_timeout_seconds diff --git a/app/services/tournament_transaction_logger.rb b/app/services/tournament_transaction_logger.rb index 90b0255..2cd1e43 100644 --- a/app/services/tournament_transaction_logger.rb +++ b/app/services/tournament_transaction_logger.rb @@ -5,9 +5,7 @@ class TournamentTransactionLogger def lock_tournament!(tournament) raise ArgumentError, 'tournament is required' if tournament.nil? - unless ActiveRecord::Base.connection.transaction_open? - raise 'Tournament transaction log writes must happen inside a transaction' - end + raise 'Tournament transaction log writes must happen inside a transaction' unless ActiveRecord::Base.connection.transaction_open? # Lock a fresh row instance so callers keep saved_change_to_* state for # after_commit callbacks on the object they just updated. diff --git a/app/services/user_service.rb b/app/services/user_service.rb index f950f0e..badcf98 100644 --- a/app/services/user_service.rb +++ b/app/services/user_service.rb @@ -7,7 +7,7 @@ class UserService def bet!(match, team) validate_bet! match, team - @user.bets.create! match: match, team: team + @user.bets.create! match:, team: end private diff --git a/config/application.rb b/config/application.rb index dbdbb57..f45f03b 100644 --- a/config/application.rb +++ b/config/application.rb @@ -50,6 +50,5 @@ module TurniereBackend config.action_mailer.default_options = { from: 'noreply@turnie.re' } - end end diff --git a/config/boot.rb b/config/boot.rb index 988a5dd..c04863f 100644 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,4 +1,6 @@ -ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__) +# frozen_string_literal: true -require "bundler/setup" # Set up gems listed in the Gemfile. -require "bootsnap/setup" # Speed up boot time by caching expensive operations. +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) + +require 'bundler/setup' # Set up gems listed in the Gemfile. +require 'bootsnap/setup' # Speed up boot time by caching expensive operations. diff --git a/config/environments/development.rb b/config/environments/development.rb index 3255142..48caece 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -16,7 +16,7 @@ Rails.application.configure do # Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. - if Rails.root.join('tmp', 'caching-dev.txt').exist? + if Rails.root.join('tmp/caching-dev.txt').exist? config.action_controller.perform_caching = true config.cache_store = :memory_store diff --git a/config/environments/production.rb b/config/environments/production.rb index 360a4b1..fca6957 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -61,7 +61,7 @@ Rails.application.configure do config.active_support.deprecation = :notify # Use default logging formatter so that PID and timestamp are not suppressed. - config.log_formatter = ::Logger::Formatter.new + config.log_formatter = Logger::Formatter.new # Use a different logger for distributed setups. # require 'syslog/logger' @@ -91,5 +91,5 @@ Rails.application.configure do end # Logging to STDOUT (running in docker by default) - config.logger = Logger.new(STDOUT) + config.logger = Logger.new($stdout) end diff --git a/config/initializers/devise_routes_rails_8_compat.rb b/config/initializers/devise_routes_rails_8_compat.rb index d460935..a7194fc 100644 --- a/config/initializers/devise_routes_rails_8_compat.rb +++ b/config/initializers/devise_routes_rails_8_compat.rb @@ -14,14 +14,14 @@ module DeviseRoutesRails8Compat def devise_password(mapping, controllers) resource :password, - only: [:new, :create, :edit, :update], + only: %i[new create edit update], path: mapping.path_names[:password], controller: controllers[:passwords] end def devise_confirmation(mapping, controllers) resource :confirmation, - only: [:new, :create, :show], + only: %i[new create show], path: mapping.path_names[:confirmation], controller: controllers[:confirmations] end @@ -30,14 +30,14 @@ module DeviseRoutesRails8Compat return unless mapping.to.unlock_strategy_enabled?(:email) resource :unlock, - only: [:new, :create, :show], + only: %i[new create show], path: mapping.path_names[:unlock], controller: controllers[:unlocks] end def devise_registration(mapping, controllers) resource :registration, - only: [:new, :create, :edit, :update, :destroy], + only: %i[new create edit update destroy], path: mapping.path_names[:registration], path_names: { new: mapping.path_names[:sign_up], diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index adc6568..3df77c5 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Configure parameters to be filtered from the log file. Use this to limit dissemination of # sensitive information. See the ActiveSupport::ParameterFilter documentation for supported # notations and behaviors. -Rails.application.config.filter_parameters += [ - :passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn +Rails.application.config.filter_parameters += %i[ + passw secret token _key crypt salt certificate otp ssn ] diff --git a/config/initializers/new_framework_defaults_7_0.rb b/config/initializers/new_framework_defaults_7_0.rb index a579326..506ccb1 100644 --- a/config/initializers/new_framework_defaults_7_0.rb +++ b/config/initializers/new_framework_defaults_7_0.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # # This file eases your Rails 7.0 framework defaults upgrade. diff --git a/config/spring.rb b/config/spring.rb index 2be7406..c7fde87 100644 --- a/config/spring.rb +++ b/config/spring.rb @@ -8,10 +8,9 @@ ].each { |path| Spring.watch(path) } Spring.after_fork do - if ENV['DEBUGGER_STORED_RUBYLIB'] - ENV['DEBUGGER_STORED_RUBYLIB'].split(File::PATH_SEPARATOR).each do |path| - next unless path =~ /ruby-debug-ide/ - load path + '/ruby-debug-ide/multiprocess/starter.rb' - end + ENV['DEBUGGER_STORED_RUBYLIB']&.split(File::PATH_SEPARATOR)&.each do |path| + next unless path.include?('ruby-debug-ide') + + load "#{path}/ruby-debug-ide/multiprocess/starter.rb" end end diff --git a/db/migrate/20220516203838_add_service_name_to_active_storage_blobs.active_storage.rb b/db/migrate/20220516203838_add_service_name_to_active_storage_blobs.active_storage.rb index a15c6ce..735df75 100644 --- a/db/migrate/20220516203838_add_service_name_to_active_storage_blobs.active_storage.rb +++ b/db/migrate/20220516203838_add_service_name_to_active_storage_blobs.active_storage.rb @@ -1,17 +1,19 @@ +# frozen_string_literal: true + # This migration comes from active_storage (originally 20190112182829) class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0] def up return unless table_exists?(:active_storage_blobs) - unless column_exists?(:active_storage_blobs, :service_name) - add_column :active_storage_blobs, :service_name, :string + return if column_exists?(:active_storage_blobs, :service_name) - if configured_service = ActiveStorage::Blob.service.name - ActiveStorage::Blob.unscoped.update_all(service_name: configured_service) - end + add_column :active_storage_blobs, :service_name, :string - change_column :active_storage_blobs, :service_name, :string, null: false + if (configured_service = ActiveStorage::Blob.service.name) + ActiveStorage::Blob.unscoped.update_all(service_name: configured_service) end + + change_column :active_storage_blobs, :service_name, :string, null: false end def down diff --git a/db/migrate/20220516203839_create_active_storage_variant_records.active_storage.rb b/db/migrate/20220516203839_create_active_storage_variant_records.active_storage.rb index 94ac83a..ca0e862 100644 --- a/db/migrate/20220516203839_create_active_storage_variant_records.active_storage.rb +++ b/db/migrate/20220516203839_create_active_storage_variant_records.active_storage.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This migration comes from active_storage (originally 20191206030411) class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0] def change @@ -8,20 +10,21 @@ class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0] t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type t.string :variation_digest, null: false - t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true + t.index %i[blob_id variation_digest], name: 'index_active_storage_variant_records_uniqueness', unique: true t.foreign_key :active_storage_blobs, column: :blob_id end end private - def primary_key_type - config = Rails.configuration.generators - config.options[config.orm][:primary_key_type] || :primary_key - end - def blobs_primary_key_type - pkey_name = connection.primary_key(:active_storage_blobs) - pkey_column = connection.columns(:active_storage_blobs).find { |c| c.name == pkey_name } - pkey_column.bigint? ? :bigint : pkey_column.type - end + def primary_key_type + config = Rails.configuration.generators + config.options[config.orm][:primary_key_type] || :primary_key + end + + def blobs_primary_key_type + pkey_name = connection.primary_key(:active_storage_blobs) + pkey_column = connection.columns(:active_storage_blobs).find { |c| c.name == pkey_name } + pkey_column.bigint? ? :bigint : pkey_column.type + end end diff --git a/db/migrate/20220516203840_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb b/db/migrate/20220516203840_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb index 93c8b85..6f31822 100644 --- a/db/migrate/20220516203840_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb +++ b/db/migrate/20220516203840_remove_not_null_on_active_storage_blobs_checksum.active_storage.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # This migration comes from active_storage (originally 20211119233751) class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0] def change diff --git a/db/migrate/20240407175005_add_position_to_group_scores.rb b/db/migrate/20240407175005_add_position_to_group_scores.rb index ab350b6..01dabba 100644 --- a/db/migrate/20240407175005_add_position_to_group_scores.rb +++ b/db/migrate/20240407175005_add_position_to_group_scores.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddPositionToGroupScores < ActiveRecord::Migration[7.0] def change add_column :group_scores, :position, :integer diff --git a/db/migrate/20240407180900_add_default_to_position_in_group_scores.rb b/db/migrate/20240407180900_add_default_to_position_in_group_scores.rb index 256b0fe..2d3d841 100644 --- a/db/migrate/20240407180900_add_default_to_position_in_group_scores.rb +++ b/db/migrate/20240407180900_add_default_to_position_in_group_scores.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddDefaultToPositionInGroupScores < ActiveRecord::Migration[7.0] def change change_column_default :group_scores, :position, 0 diff --git a/db/migrate/20250309202658_add_timer_end_to_tournaments.rb b/db/migrate/20250309202658_add_timer_end_to_tournaments.rb index 1762bec..09a70df 100644 --- a/db/migrate/20250309202658_add_timer_end_to_tournaments.rb +++ b/db/migrate/20250309202658_add_timer_end_to_tournaments.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddTimerEndToTournaments < ActiveRecord::Migration[7.0] def change add_column :tournaments, :timer_end, :datetime diff --git a/db/migrate/20260422100000_create_team_action_lists.rb b/db/migrate/20260422100000_create_team_action_lists.rb index fa7547d..0382dc7 100644 --- a/db/migrate/20260422100000_create_team_action_lists.rb +++ b/db/migrate/20260422100000_create_team_action_lists.rb @@ -13,7 +13,7 @@ class CreateTeamActionLists < ActiveRecord::Migration[7.0] t.timestamps end - add_index :team_action_lists, [:tournament_id, :key], unique: true + add_index :team_action_lists, %i[tournament_id key], unique: true create_table :team_action_items do |t| t.references :team_action_list, null: false, foreign_key: { on_delete: :cascade }, index: true @@ -25,6 +25,6 @@ class CreateTeamActionLists < ActiveRecord::Migration[7.0] t.timestamps end - add_index :team_action_items, [:team_action_list_id, :team_id], unique: true + add_index :team_action_items, %i[team_action_list_id team_id], unique: true end end diff --git a/db/migrate/20260423133000_add_timer_mode_to_tournaments.rb b/db/migrate/20260423133000_add_timer_mode_to_tournaments.rb index 07f5ef2..35eb0c0 100644 --- a/db/migrate/20260423133000_add_timer_mode_to_tournaments.rb +++ b/db/migrate/20260423133000_add_timer_mode_to_tournaments.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddTimerModeToTournaments < ActiveRecord::Migration[7.0] def change add_column :tournaments, :timer_mode, :string diff --git a/db/migrate/20260430120000_add_sync_auth_token_digest_to_tournaments.rb b/db/migrate/20260430120000_add_sync_auth_token_digest_to_tournaments.rb index 9e514f6..677ef80 100644 --- a/db/migrate/20260430120000_add_sync_auth_token_digest_to_tournaments.rb +++ b/db/migrate/20260430120000_add_sync_auth_token_digest_to_tournaments.rb @@ -8,9 +8,7 @@ class AddSyncAuthTokenDigestToTournaments < ActiveRecord::Migration[7.0] end def up - unless column_exists?(:tournaments, :sync_auth_token_digest) - add_column :tournaments, :sync_auth_token_digest, :string - end + add_column :tournaments, :sync_auth_token_digest, :string unless column_exists?(:tournaments, :sync_auth_token_digest) MigrationTournament.reset_column_information MigrationTournament diff --git a/db/migrate/20260430121000_create_tournament_transaction_log_entries.rb b/db/migrate/20260430121000_create_tournament_transaction_log_entries.rb index ed194c8..141b498 100644 --- a/db/migrate/20260430121000_create_tournament_transaction_log_entries.rb +++ b/db/migrate/20260430121000_create_tournament_transaction_log_entries.rb @@ -14,7 +14,7 @@ class CreateTournamentTransactionLogEntries < ActiveRecord::Migration[7.0] end add_index :tournament_transaction_log_entries, - [:tournament_id, :sequence], + %i[tournament_id sequence], unique: true, name: 'index_tournament_log_entries_on_tournament_and_sequence' end diff --git a/e2e/lib/action_cable_client.rb b/e2e/lib/action_cable_client.rb index 47a7be0..f3d9f80 100644 --- a/e2e/lib/action_cable_client.rb +++ b/e2e/lib/action_cable_client.rb @@ -11,7 +11,7 @@ module TurniereE2E ACTION_CABLE_PROTOCOLS = %w[actioncable-v1-json actioncable-unsupported].freeze def initialize(base_url:) - @base_url = base_url.sub(%r{/\z}, '') + @base_url = base_url.delete_suffix('/') @messages = Queue.new @closed = false end @@ -19,7 +19,7 @@ module TurniereE2E def subscribe_team_action_lists!(tournament_id:) identifier = { channel: 'TournamentTeamActionListsChannel', - tournament_id: tournament_id + tournament_id: }.to_json subscribe!(identifier:) do |message| @@ -30,7 +30,7 @@ module TurniereE2E def subscribe_timer!(tournament_id:) identifier = { channel: 'TournamentTimersChannel', - tournament_id: tournament_id + tournament_id: }.to_json subscribe!(identifier:) do |message| @@ -41,8 +41,8 @@ module TurniereE2E def subscribe_beamer!(tournament_id:, beamer_name:) identifier = { channel: 'TournamentBeamersChannel', - tournament_id: tournament_id, - beamer_name: beamer_name + tournament_id:, + beamer_name: }.to_json subscribe!(identifier:) do |message| @@ -78,31 +78,31 @@ module TurniereE2E end def wait_for_payload!(timeout: 20) - wait_for!(timeout: timeout) do |message| + wait_for!(timeout:) do |message| team_action_lists_payload_message?(message) end.fetch('message') end def wait_for_timer_payload!(timeout: 20) - wait_for!(timeout: timeout) do |message| + wait_for!(timeout:) do |message| timer_payload_message?(message) end.fetch('message') end def wait_for_beamer_payload!(timeout: 20) - wait_for!(timeout: timeout) do |message| + wait_for!(timeout:) do |message| beamer_payload_message?(message) end.fetch('message') end def wait_for_tournament_payload!(timeout: 20) - wait_for!(timeout: timeout) do |message| + wait_for!(timeout:) do |message| tournament_payload_message?(message) end.fetch('message') end def wait_for_matches_payload!(timeout: 20) - wait_for!(timeout: timeout) do |message| + wait_for!(timeout:) do |message| matches_payload_message?(message) end.fetch('message') end @@ -135,7 +135,7 @@ module TurniereE2E driver.text( JSON.dump( command: 'subscribe', - identifier: identifier + identifier: ) ) @@ -184,8 +184,8 @@ module TurniereE2E end def pump_until_open!(timeout: 10) - deadline = Time.now + timeout - while Time.now < deadline + deadline = monotonic_deadline(timeout) + while Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline read_from_socket(timeout: 0.5) return if driver.ready_state == 1 end @@ -194,14 +194,14 @@ module TurniereE2E end def wait_for!(timeout: 20) - deadline = Time.now + timeout + deadline = monotonic_deadline(timeout) loop do until @messages.empty? message = @messages.pop(true) return message if yield(message) end - raise "websocket message not received within #{timeout}s" if Time.now >= deadline + raise "websocket message not received within #{timeout}s" if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline read_from_socket(timeout: 0.5) rescue ThreadError @@ -209,6 +209,10 @@ module TurniereE2E end end + def monotonic_deadline(timeout) + Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout + end + def read_from_socket(timeout:) readable, = IO.select([socket], nil, nil, timeout) return if readable.nil? diff --git a/e2e/lib/api_client.rb b/e2e/lib/api_client.rb index 0fef340..6ea0198 100644 --- a/e2e/lib/api_client.rb +++ b/e2e/lib/api_client.rb @@ -19,7 +19,7 @@ module TurniereE2E attr_reader :base_url def initialize(base_url:) - @base_url = base_url.sub(%r{/\z}, '') + @base_url = base_url.delete_suffix('/') @auth_headers = {} @login_credentials = nil end @@ -29,10 +29,10 @@ module TurniereE2E :post, '/users', body: { - email: email, - password: password, + email:, + password:, password_confirmation: password, - username: username + username: } ) store_auth_headers!(response) @@ -40,14 +40,14 @@ module TurniereE2E end def login!(email:, password:) - @login_credentials = { email: email, password: password } + @login_credentials = { email:, password: } @auth_headers = {} response = request( :post, '/users/sign_in', body: { - email: email, - password: password + email:, + password: } ) store_auth_headers!(response) @@ -59,19 +59,19 @@ module TurniereE2E end def get(path, params: nil, headers: {}) - request(:get, path, params: params, headers: headers) + request(:get, path, params:, headers:) end def post(path, body: nil, params: nil, headers: {}) - request(:post, path, body: body, params: params, headers: headers) + request(:post, path, body:, params:, headers:) end def patch(path, body: nil, params: nil, headers: {}) - request(:patch, path, body: body, params: params, headers: headers) + request(:patch, path, body:, params:, headers:) end def delete(path, params: nil, headers: {}) - request(:delete, path, params: params, headers: headers) + request(:delete, path, params:, headers:) end private @@ -80,7 +80,7 @@ module TurniereE2E login!(**@login_credentials) if refresh_session && should_refresh_session?(method, path) uri = URI.parse("#{base_url}#{path}") - uri.query = URI.encode_www_form(params) if params && !params.empty? + uri.query = URI.encode_www_form(params) if params&.any? request_class = case method when :get then Net::HTTP::Get @@ -108,7 +108,8 @@ module TurniereE2E store_auth_headers!(result) if retry_on_unauthorized && result[:status] == 401 && can_reauthenticate?(path) login!(**@login_credentials) - return request(method, path, body: body, params: params, headers: headers, retry_on_unauthorized: false, refresh_session: false) + return request(method, path, body:, params:, headers:, retry_on_unauthorized: false, + refresh_session: false) end result end diff --git a/e2e/lib/load_test_runner.rb b/e2e/lib/load_test_runner.rb index ddcb77f..ea93da4 100644 --- a/e2e/lib/load_test_runner.rb +++ b/e2e/lib/load_test_runner.rb @@ -267,12 +267,8 @@ module TurniereE2E def websocket_failures(level) failures = [] - if level.fetch(:errors).positive? - failures << "WS #{level.fetch(:clients)} clients: #{level.fetch(:errors)} failed" - end - if websocket_open_short?(level) - failures << "WS #{level.fetch(:clients)} clients: only #{level.fetch(:opened)} opened" - end + failures << "WS #{level.fetch(:clients)} clients: #{level.fetch(:errors)} failed" if level.fetch(:errors).positive? + failures << "WS #{level.fetch(:clients)} clients: only #{level.fetch(:opened)} opened" if websocket_open_short?(level) if websocket_metric_short?(level) failures << "WS #{level.fetch(:clients)} clients: metrics saw #{level.fetch(:connection_metric)} connections" end diff --git a/e2e/lib/scenario_runner.rb b/e2e/lib/scenario_runner.rb index cee171a..7d68af9 100644 --- a/e2e/lib/scenario_runner.rb +++ b/e2e/lib/scenario_runner.rb @@ -7,7 +7,7 @@ require 'uri' module TurniereE2E class ScenarioRunner - DEFAULT_PASSWORD = 'password123'.freeze + DEFAULT_PASSWORD = 'password123' attr_reader :client @@ -36,19 +36,19 @@ module TurniereE2E timestamp: timer_response.fetch(:timestamp), timer_reason: timer_response.fetch(:timer_reason) } - return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :timer_set + return checkpoint_result(session, tournament, checkpoints, stop_at) if stop_at == :timer_set upcoming_matches = get!( "/tournaments/#{tournament.fetch(:id)}/matches", params: { state: 'upcoming' } ) checkpoints[:upcoming_group_matches] = { match_ids: upcoming_matches.map { |match| match.fetch(:id) } } - return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :upcoming_group_matches + return checkpoint_result(session, tournament, checkpoints, stop_at) if stop_at == :upcoming_group_matches finish_group_stage_matches!(tournament) tournament = fetch_tournament!(tournament.fetch(:id)) checkpoints[:groups_finished] = summarize_tournament(tournament) - return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :groups_finished + return checkpoint_result(session, tournament, checkpoints, stop_at) if stop_at == :groups_finished statistics = get!("/tournaments/#{tournament.fetch(:id)}/statistics") checkpoints[:statistics] = { @@ -60,12 +60,12 @@ module TurniereE2E finish_stage!(group_stage_of!(tournament).fetch(:id)) tournament = fetch_tournament!(tournament.fetch(:id)) checkpoints[:playoffs_created] = summarize_tournament(tournament) - return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :playoffs_created + return checkpoint_result(session, tournament, checkpoints, stop_at) if stop_at == :playoffs_created run_playoffs_until_before_final!(tournament.fetch(:id)) tournament = fetch_tournament!(tournament.fetch(:id)) checkpoints[:before_final_finish_attempt] = summarize_tournament(tournament) - return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :before_final_finish_attempt + return checkpoint_result(session, tournament, checkpoints, stop_at) if stop_at == :before_final_finish_attempt finish_attempt = finish_final_stage_matches!(tournament) tournament = fetch_tournament!(tournament.fetch(:id)) @@ -86,18 +86,27 @@ module TurniereE2E checkpoints = { created: summarize_tournament(tournament) } - return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :created + if stop_at == :created + return result(session: session, tournament: tournament, checkpoints: checkpoints, + stop_at: stop_at) + end finish_group_stage_matches!(tournament) finish_stage!(group_stage_of!(fetch_tournament!(tournament.fetch(:id))).fetch(:id)) tournament = fetch_tournament!(tournament.fetch(:id)) checkpoints[:playoffs_created] = summarize_tournament(tournament) - return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :playoffs_created + if stop_at == :playoffs_created + return result(session: session, tournament: tournament, checkpoints: checkpoints, + stop_at: stop_at) + end run_playoffs_until_before_final!(tournament.fetch(:id)) tournament = fetch_tournament!(tournament.fetch(:id)) checkpoints[:before_final_finish_attempt] = summarize_tournament(tournament) - return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :before_final_finish_attempt + if stop_at == :before_final_finish_attempt + return result(session: session, tournament: tournament, checkpoints: checkpoints, + stop_at: stop_at) + end finish_attempt = finish_final_stage_matches!(tournament) tournament = fetch_tournament!(tournament.fetch(:id)) @@ -132,7 +141,10 @@ module TurniereE2E checkpoints[created_key] = summarize_tournament(tournament).merge( playoff_matchups: playoff_matchups(tournament) ) - return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == created_key + if stop_at == created_key + return result(session: session, tournament: tournament, checkpoints: checkpoints, + stop_at: stop_at) + end next if index == rounds.size - 1 @@ -140,7 +152,10 @@ module TurniereE2E tournament = fetch_tournament!(tournament.fetch(:id)) deleted_key = deleted_keys.fetch(index) checkpoints[deleted_key] = summarize_tournament(tournament) - return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == deleted_key + if stop_at == deleted_key + return result(session: session, tournament: tournament, checkpoints: checkpoints, + stop_at: stop_at) + end end result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) @@ -152,7 +167,10 @@ module TurniereE2E checkpoints = { created: summarize_tournament(tournament) } - return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :created + if stop_at == :created + return result(session: session, tournament: tournament, checkpoints: checkpoints, + stop_at: stop_at) + end run_playoffs_until_complete!(tournament.fetch(:id)) tournament = fetch_tournament!(tournament.fetch(:id)) @@ -170,7 +188,10 @@ module TurniereE2E checkpoints = { created: summarize_tournament(tournament) } - return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :created + if stop_at == :created + return result(session: session, tournament: tournament, checkpoints: checkpoints, + stop_at: stop_at) + end group = group_stage_of!(tournament).fetch(:groups).first play_group_matches_with_explicit_scores!(group, group_direct_comparison_score_plan(case_name)) @@ -250,7 +271,10 @@ module TurniereE2E checkpoints = { created: summarize_tournament(tournament) } - return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :created + if stop_at == :created + return result(session: session, tournament: tournament, checkpoints: checkpoints, + stop_at: stop_at) + end profiled_show = fetch_tournament_profile!(tournament.fetch(:id)) checkpoints[:profiled_show] = profiled_show @@ -259,7 +283,7 @@ module TurniereE2E end def wait_for_healthcheck!(timeout_seconds: 30) - started_at = Time.now + started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) loop do begin response = client.get('/healthz') @@ -268,7 +292,8 @@ module TurniereE2E nil end - raise "healthcheck did not become ready within #{timeout_seconds}s" if Time.now - started_at > timeout_seconds + elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at + raise "healthcheck did not become ready within #{timeout_seconds}s" if elapsed > timeout_seconds sleep 0.5 end @@ -282,7 +307,7 @@ module TurniereE2E unique = SecureRandom.hex(6) email = "e2e-#{unique}@example.com" username = "e2e-#{unique}" - response = client.register!(email: email, password: DEFAULT_PASSWORD, username: username) + client.register!(email: email, password: DEFAULT_PASSWORD, username: username) return { email: email, username: username } if client.authenticated? login_response = client.login!(email: email, password: DEFAULT_PASSWORD) @@ -350,9 +375,12 @@ module TurniereE2E def run_playoffs_until_before_final!(tournament_id) loop do tournament = fetch_tournament!(tournament_id) - playable_matches = playoff_matches(tournament).reject { |match| match.fetch(:stage_level) == 0 } - .select { |match| match.fetch(:state) == 'not_started' } - .sort_by { |match| [-match.fetch(:stage_level), match.fetch(:position)] } + playable_matches = playoff_matches(tournament).reject { |match| match.fetch(:stage_level).zero? } + .select { |match| match.fetch(:state) == 'not_started' } + .sort_by do |match| + [-match.fetch(:stage_level), + match.fetch(:position)] + end break if playable_matches.empty? @@ -366,7 +394,10 @@ module TurniereE2E loop do tournament = fetch_tournament!(tournament_id) playable_matches = playoff_matches(tournament).select { |match| match.fetch(:state) == 'not_started' } - .sort_by { |match| [-match.fetch(:stage_level), match.fetch(:position)] } + .sort_by do |match| + [-match.fetch(:stage_level), + match.fetch(:position)] + end break if playable_matches.empty? playable_matches.each do |match| @@ -390,7 +421,9 @@ module TurniereE2E expect_status!(finished, expected_finish_status, "finish match #{match.fetch(:id)}") if expected_finish_error actual_error = finished.fetch(:json).fetch(:error) - raise "expected finish error #{expected_finish_error.inspect}, got #{actual_error.inspect}" unless actual_error == expected_finish_error + unless actual_error == expected_finish_error + raise "expected finish error #{expected_finish_error.inspect}, got #{actual_error.inspect}" + end end { @@ -418,7 +451,9 @@ module TurniereE2E expect_status!(finished, expected_finish_status, "finish match #{match.fetch(:id)}") if expected_finish_error actual_error = finished.fetch(:json).fetch(:error) - raise "expected finish error #{expected_finish_error.inspect}, got #{actual_error.inspect}" unless actual_error == expected_finish_error + unless actual_error == expected_finish_error + raise "expected finish error #{expected_finish_error.inspect}, got #{actual_error.inspect}" + end end { @@ -481,7 +516,9 @@ module TurniereE2E end def final_stage_matches_of!(tournament) - final_stage = tournament.fetch(:stages).find { |stage| stage.fetch(:level) == 0 } || raise('final stage not found') + final_stage = tournament.fetch(:stages).find do |stage| + stage.fetch(:level).zero? + end || raise('final stage not found') attach_stage_level(final_stage).fetch(:matches).sort_by { |match| match.fetch(:position) } end @@ -496,7 +533,9 @@ module TurniereE2E { status: 200, - finished_match_ids: final_stage_matches_of!(fetch_tournament!(tournament.fetch(:id))).map { |match| match.fetch(:id) } + finished_match_ids: final_stage_matches_of!(fetch_tournament!(tournament.fetch(:id))).map do |match| + match.fetch(:id) + end } end @@ -574,7 +613,7 @@ module TurniereE2E def create_group_stage_payload(group_count:, teams_per_group:, playoff_teams_amount:) teams = group_count.times.flat_map do |group_index| - teams_per_group.times.map do |seed_index| + Array.new(teams_per_group) do |seed_index| { name: "G#{group_index + 1}-S#{seed_index + 1}", group: group_index @@ -626,7 +665,7 @@ module TurniereE2E name: "HTTP E2E Playoff Only #{team_count} #{SecureRandom.hex(3)}", description: 'HTTP lifecycle E2E tournament', public: false, - teams: team_count.times.map { |index| { name: "G0-S#{index + 1}" } } + teams: Array.new(team_count) { |index| { name: "G0-S#{index + 1}" } } } end @@ -739,7 +778,7 @@ module TurniereE2E end def parse_server_timing(header) - return {} if header.nil? || header.empty? + return {} if header.to_s.empty? header.split(',').each_with_object({}) do |entry, sections| name, duration = entry.strip.match(/\A([^;]+);dur=([0-9.]+)\z/)&.captures @@ -758,6 +797,10 @@ module TurniereE2E } end + def checkpoint_result(session, tournament, checkpoints, stop_at) + result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) + end + def expect_status!(response, expected_status, action) return if response.fetch(:status) == expected_status diff --git a/e2e/load_test.rb b/e2e/load_test.rb index 1ca9e07..def4057 100755 --- a/e2e/load_test.rb +++ b/e2e/load_test.rb @@ -73,32 +73,31 @@ result = runner.run if options[:format] == 'json' puts JSON.pretty_generate(result) - exit -end +else + puts "Load test base_url=#{result.fetch(:base_url)} tournament_id=#{result.dig(:tournament, :id)}" -puts "Load test base_url=#{result.fetch(:base_url)} tournament_id=#{result.dig(:tournament, :id)}" + result.fetch(:http).each do |level| + puts( + "HTTP #{level.fetch(:clients)} clients: " \ + "#{level.fetch(:ok)}/#{level.fetch(:requests)} ok, errors=#{level.fetch(:errors)}, " \ + "p50=#{level.fetch(:p50_ms)}ms, p95=#{level.fetch(:p95_ms)}ms, " \ + "max=#{level.fetch(:max_ms)}ms, rps=#{level.fetch(:requests_per_second)}" + ) + end -result.fetch(:http).each do |level| + result.fetch(:websocket).each do |level| + puts( + "WS #{level.fetch(:clients)} clients: " \ + "opened=#{level.fetch(:opened)}, errors=#{level.fetch(:errors)}, " \ + "p50_open=#{level.fetch(:p50_open_ms)}ms, p95_open=#{level.fetch(:p95_open_ms)}ms, " \ + "connections_metric=#{level.fetch(:connection_metric)}" + ) + end + + metrics = result.fetch(:metrics) puts( - "HTTP #{level.fetch(:clients)} clients: " \ - "#{level.fetch(:ok)}/#{level.fetch(:requests)} ok, errors=#{level.fetch(:errors)}, " \ - "p50=#{level.fetch(:p50_ms)}ms, p95=#{level.fetch(:p95_ms)}ms, " \ - "max=#{level.fetch(:max_ms)}ms, rps=#{level.fetch(:requests_per_second)}" + "Metrics: action_cable_connections=#{metrics.fetch(:action_cable_connections)}, " \ + "action_cable_connections_total=#{metrics.fetch(:action_cable_connections_total)}, " \ + "ruby_threads=#{metrics.fetch(:ruby_threads)}" ) end - -result.fetch(:websocket).each do |level| - puts( - "WS #{level.fetch(:clients)} clients: " \ - "opened=#{level.fetch(:opened)}, errors=#{level.fetch(:errors)}, " \ - "p50_open=#{level.fetch(:p50_open_ms)}ms, p95_open=#{level.fetch(:p95_open_ms)}ms, " \ - "connections_metric=#{level.fetch(:connection_metric)}" - ) -end - -metrics = result.fetch(:metrics) -puts( - "Metrics: action_cable_connections=#{metrics.fetch(:action_cable_connections)}, " \ - "action_cable_connections_total=#{metrics.fetch(:action_cable_connections_total)}, " \ - "ruby_threads=#{metrics.fetch(:ruby_threads)}" -) diff --git a/lib/local/request_profiling.rb b/lib/local/request_profiling.rb index d231eea..1f8cf9f 100644 --- a/lib/local/request_profiling.rb +++ b/lib/local/request_profiling.rb @@ -28,7 +28,7 @@ module Local result = yield end duration = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000.0 - @measurements << { name: name, duration: duration.round(1), query_count: query_count } + @measurements << { name:, duration: duration.round(1), query_count: } result end diff --git a/script/e2e_scenarios.rb b/script/e2e_scenarios.rb old mode 100644 new mode 100755 index 45da011..c35102b --- a/script/e2e_scenarios.rb +++ b/script/e2e_scenarios.rb @@ -5,6 +5,11 @@ require 'json' require 'optparse' require_relative '../e2e/lib/scenario_runner' +def fail_with(message) + warn message + raise SystemExit, 1 +end + options = { base_url: ENV.fetch('TURNIERE_E2E_BASE_URL', 'http://127.0.0.1:3000'), stop_at: nil, @@ -12,9 +17,9 @@ options = { group_count: 4, teams_per_group: 4, playoff_teams_amount: 8, - email: ENV['TURNIERE_E2E_EMAIL'], - password: ENV['TURNIERE_E2E_PASSWORD'], - username: ENV['TURNIERE_E2E_USERNAME'] + email: ENV.fetch('TURNIERE_E2E_EMAIL', nil), + password: ENV.fetch('TURNIERE_E2E_PASSWORD', nil), + username: ENV.fetch('TURNIERE_E2E_USERNAME', nil) } OptionParser.new do |parser| @@ -22,9 +27,15 @@ OptionParser.new do |parser| parser.on('--base-url URL', 'API base URL') { |value| options[:base_url] = value } parser.on('--stop-at CHECKPOINT', 'Stop scenario at a named checkpoint') { |value| options[:stop_at] = value.to_sym } - parser.on('--team-count COUNT', Integer, 'Team count for playoff_only scenario') { |value| options[:team_count] = value } - parser.on('--group-count COUNT', Integer, 'Group count for group-stage scenarios') { |value| options[:group_count] = value } - parser.on('--teams-per-group COUNT', Integer, 'Teams per group for group-stage scenarios') { |value| options[:teams_per_group] = value } + parser.on('--team-count COUNT', Integer, 'Team count for playoff_only scenario') do |value| + options[:team_count] = value + end + parser.on('--group-count COUNT', Integer, 'Group count for group-stage scenarios') do |value| + options[:group_count] = value + end + parser.on('--teams-per-group COUNT', Integer, 'Teams per group for group-stage scenarios') do |value| + options[:teams_per_group] = value + end parser.on('--playoff-teams-amount COUNT', Integer, 'Playoff teams amount for group-stage scenarios') do |value| options[:playoff_teams_amount] = value end @@ -33,7 +44,9 @@ OptionParser.new do |parser| parser.on('--username USERNAME', 'Confirmed E2E user username') { |value| options[:username] = value } end.parse! -scenario = ARGV.shift or abort('Missing scenario name') +scenario = ARGV.shift +fail_with('Missing scenario name') if scenario.nil? + runner = TurniereE2E::ScenarioRunner.new( base_url: options[:base_url], email: options[:email], @@ -58,7 +71,7 @@ result = case scenario stop_at: options[:stop_at] || :profiled_show ) else - abort("Unknown scenario #{scenario.inspect}") + fail_with("Unknown scenario #{scenario.inspect}") end puts JSON.pretty_generate(result) diff --git a/script/wait_for_http.rb b/script/wait_for_http.rb old mode 100644 new mode 100755 diff --git a/tasks.py b/tasks.py index d8c02e0..e0b3dec 100644 --- a/tasks.py +++ b/tasks.py @@ -57,6 +57,18 @@ BLACKBOX_MAILGUN_API_KEY = "blackbox-test-api-key" BLACKBOX_MAILGUN_DOMAIN = "blackbox.example.com" SPEC_ROOT = Path("spec") E2E_SPEC_ROOT = SPEC_ROOT / "e2e" / "http" +LINT_TARGETS = ( + "Gemfile", + "Rakefile", + "config.ru", + "app", + "config", + "db/migrate", + "db/seeds.rb", + "e2e", + "lib", + "script", +) ROOT = Path(__file__).resolve().parent DB_DIR = ROOT / "db" GEMFILE_LOCK_PATH = ROOT / "Gemfile.lock" @@ -802,7 +814,7 @@ def test_shard(c, node_index=1, node_total=8, dry_run=False): @task def lint(c): """Run RuboCop.""" - c.run(_bundle_exec("rubocop"), pty=True) + c.run(_bundle_exec("rubocop", *LINT_TARGETS, "--cache", "false"), pty=True) @task(name="bootstrap-dev-user")