Merge branch 'codex/TUR-113-fix-rubocop' into 'master'

TUR-113: Fix RuboCop

Closes TUR-113

See merge request turniere/turniere-backend!59
This commit is contained in:
Daniel Schädler 2026-05-06 14:02:37 +00:00
commit b8518a1242
51 changed files with 382 additions and 817 deletions

View File

@ -7,7 +7,6 @@ db/*.sqlite3.*
docker/** docker/**
.gitignore .gitignore
.hound.yml .hound.yml
.rubocop.yml
docker-compose.yml docker-compose.yml
README.md README.md
.gitlab-ci.yml .gitlab-ci.yml

View File

@ -13,6 +13,21 @@ include:
- project: 'turniere/turniere-infra' - project: 'turniere/turniere-infra'
file: '/ci/pipeline.yaml' 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: rails spec:
stage: test stage: test
tags: tags:

View File

@ -1,9 +1,3 @@
inherit_from: .rubocop_todo.yml
inherit_mode:
merge:
- Exclude
plugins: plugins:
- rubocop-performance - rubocop-performance
- rubocop-rails - rubocop-rails
@ -15,6 +9,7 @@ AllCops:
Exclude: Exclude:
- "bin/**/*" - "bin/**/*"
- "db/schema.rb" - "db/schema.rb"
- "spec/**/*.rb"
# Enable Rails # Enable Rails
Rails: Rails:
@ -22,14 +17,14 @@ Rails:
# Commonly used screens these days easily fit more than 80 characters. # Commonly used screens these days easily fit more than 80 characters.
Layout/LineLength: Layout/LineLength:
Max: 120 Max: 140
# Too short methods lead to extraction of single-use methods, which can make # 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 # the code easier to read (by naming things), but can also clutter the class
Metrics/MethodLength: Metrics/MethodLength:
Exclude: Exclude:
- "db/migrate/*" - "db/migrate/*"
Max: 50 Max: 60
# The guiding principle of classes is SRP, SRP can't be accurately measured by LoC # The guiding principle of classes is SRP, SRP can't be accurately measured by LoC
Metrics/ClassLength: Metrics/ClassLength:
@ -41,9 +36,10 @@ Metrics/ModuleLength:
Metrics/AbcSize: Metrics/AbcSize:
Exclude: Exclude:
- "db/migrate/*" - "db/migrate/*"
Max: 50 Max: 75
Metrics/BlockLength: Metrics/BlockLength:
Max: 30
AllowedMethods: AllowedMethods:
- "namespace" - "namespace"
- "create_table" - "create_table"
@ -51,6 +47,42 @@ Metrics/BlockLength:
- "config/**/*.rb" - "config/**/*.rb"
- "spec/**/*.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 # Disable documentation checks for now
# should be done with yard instead # should be done with yard instead
Style/Documentation: Style/Documentation:

View File

@ -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'

View File

@ -111,6 +111,7 @@ COPY script /app/script
COPY config.ru Rakefile /app/ COPY config.ru Rakefile /app/
COPY config/environments/production.rb /app/config/environments/beta.rb COPY config/environments/production.rb /app/config/environments/beta.rb
COPY tasks.py /app/tasks.py COPY tasks.py /app/tasks.py
COPY .rubocop.yml /app/.rubocop.yml
COPY e2e /app/e2e COPY e2e /app/e2e
COPY lib /app/lib COPY lib /app/lib
COPY spec /app/spec COPY spec /app/spec

View File

@ -44,8 +44,8 @@ group :test, optional: true do
gem 'factory_bot_rails' gem 'factory_bot_rails'
gem 'faker' gem 'faker'
gem 'rspec-rails' gem 'rspec-rails'
gem 'shoulda-matchers'
gem 'ruby-prof' gem 'ruby-prof'
gem 'shoulda-matchers'
end end
group :development, optional: true do 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 # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
gem 'railroady' gem 'railroady'
# Use sqlite3 as the database for Active Record # Use sqlite3 as the database for Active Record
gem 'sqlite3'
gem 'rubocop' gem 'rubocop'
gem 'rubocop-performance' gem 'rubocop-performance'
gem 'rubocop-rails' gem 'rubocop-rails'
gem 'solargraph' gem 'solargraph'
gem 'spring' gem 'spring'
gem 'spring-watcher-listen', '~> 2.1' gem 'spring-watcher-listen', '~> 2.1'
gem 'sqlite3'
end end
group :production do group :production do

View File

@ -38,7 +38,8 @@ class MatchesController < ApplicationController
elsif match.save elsif match.save
UpdateGroupsGroupScoresAndSave.call(group: @group) UpdateGroupsGroupScoresAndSave.call(group: @group)
group_scores = group_scores_for_log(@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 synced_tournament = @group.tournament
render json: match, include: %w[match_scores.team group], status: :created render json: match, include: %w[match_scores.team group], status: :created
else else
@ -90,7 +91,8 @@ class MatchesController < ApplicationController
# PATCH /matches/1/swap # PATCH /matches/1/swap
def swap def swap
unless @match.swappable_with?(@swap_target) 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 return
end end
@ -107,7 +109,8 @@ class MatchesController < ApplicationController
end end
swapped_matches = Match.where(id: [@match.id, @swap_target.id]).order(:position) 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) push_sync_if_needed!(synced_tournament)
TournamentLiveBroadcast.call(synced_tournament.id) TournamentLiveBroadcast.call(synced_tournament.id)
end end
@ -117,9 +120,7 @@ class MatchesController < ApplicationController
def handle_match_end def handle_match_end
if @match.group_match? if @match.group_match?
group = @match.group group = @match.group
unless UpdateGroupsGroupScoresAndSave.call(group: group).success? logger.warn "Updating groups group score failed for #{group}" unless UpdateGroupsGroupScoresAndSave.call(group: group).success?
logger.warn "Updating groups group score failed for #{group}"
end
@transaction_log_group_scores = group_scores_for_log(group) @transaction_log_group_scores = group_scores_for_log(group)
return return
end end
@ -290,9 +291,7 @@ class MatchesController < ApplicationController
decider: true, decider: true,
match_scores: teams.map { |team| MatchScore.new(team: team) } match_scores: teams.map { |team| MatchScore.new(team: team) }
).tap do |match| ).tap do |match|
if team_ids.size != 2 || teams.size != 2 match.errors.add(:team_ids, 'must contain exactly two teams from this group') if team_ids.size != 2 || teams.size != 2
match.errors.add(:team_ids, 'must contain exactly two teams from this group')
end
end end
end end

View File

@ -71,9 +71,7 @@ class TeamsController < ApplicationController
return [] if id_values.empty? return [] if id_values.empty?
User.where(id: id_values).tap do |players| 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') unless players.size == id_values.uniq.size
raise ActiveRecord::RecordNotFound.new('Could not find all players', 'User')
end
end end
end end

View File

@ -14,7 +14,8 @@ class TournamentsController < ApplicationController
before_action :set_tournament_for_show, only: %i[show] before_action :set_tournament_for_show, only: %i[show]
before_action :set_tournament, before_action :set_tournament,
only: %i[update destroy destroy_playoffs set_timer timer sync_state test_sync start_next_group_matches] 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 }, before_action -> { require_owner! @tournament.owner },
only: %i[update destroy destroy_playoffs set_timer test_sync start_next_group_matches] only: %i[update destroy destroy_playoffs set_timer test_sync start_next_group_matches]
before_action :validate_create_params, only: %i[create] before_action :validate_create_params, only: %i[create]
@ -74,6 +75,7 @@ class TournamentsController < ApplicationController
end end
raise ActionController::ParameterMissing, :teams if teams.nil? raise ActionController::ParameterMissing, :teams if teams.nil?
if group_stage if group_stage
params.require(:playoff_teams_amount) params.require(:playoff_teams_amount)
groups = organize_teams_in_groups(teams) groups = organize_teams_in_groups(teams)
@ -339,7 +341,7 @@ class TournamentsController < ApplicationController
return if ActiveModel::Type::Boolean.new.cast(params[:read_only_mode]) return if ActiveModel::Type::Boolean.new.cast(params[:read_only_mode])
teams = params['teams'] 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 render json: { error: 'Invalid teams array' }, status: :unprocessable_content
end end
@ -374,12 +376,16 @@ class TournamentsController < ApplicationController
error: 'playoff_teams_amount, instant_finalists_amount and intermediate_round_participants_amount don\'t match' error: 'playoff_teams_amount, instant_finalists_amount and intermediate_round_participants_amount don\'t match'
}, status: :unprocessable_content }, status: :unprocessable_content
end end
def sync_state_params def sync_state_params
params.require(:snapshot).permit!.to_h params.require(:snapshot).permit!.to_h
end end
def authorize_sync_request! 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 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? 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] timer_reason_text = params[:timer_reason_text]
clear_timer = ActiveModel::Type::Boolean.new.cast(params[:clear_timer]) 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) if clear_timer
return validate_clear_timer_params(timestamp, timestamp_seconds, timer_mode, timer_reason, timer_reason_text)
end
if timer_mode.present? && !Tournament::TIMER_MODES.include?(timer_mode) return render_timer_validation_error('Invalid timer_mode') if timer_mode.present? && !Tournament::TIMER_MODES.include?(timer_mode)
return render_timer_validation_error('Invalid timer_mode')
end
timer_reason_error = validate_timer_reason_params( timer_reason_error = validate_timer_reason_params(
timer_reason, timer_reason,
@ -441,7 +443,20 @@ class TournamentsController < ApplicationController
) )
return render_timer_validation_error(timer_reason_error) if timer_reason_error 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? if timestamp.present? && timestamp_seconds.present?
return render_timer_validation_error('Only one of timestamp or timestamp_seconds is allowed') return render_timer_validation_error('Only one of timestamp or timestamp_seconds is allowed')
end end
@ -450,22 +465,23 @@ class TournamentsController < ApplicationController
return render_timer_validation_error('timestamp_seconds is only allowed for countdown mode') return render_timer_validation_error('timestamp_seconds is only allowed for countdown mode')
end end
if timestamp_seconds.present? return validate_timestamp_seconds_param(timestamp_seconds) if timestamp_seconds.present?
begin return validate_timestamp_param(timestamp, timer_mode) if timestamp.present?
timestamp_seconds = Integer(timestamp_seconds)
rescue ArgumentError false
return render_timer_validation_error('Invalid seconds format')
end end
if timestamp_seconds <= 0 def validate_timestamp_seconds_param(timestamp_seconds)
return render_timer_validation_error('Timestamp must be in the future for countdown mode') seconds = Integer(timestamp_seconds)
end return render_timer_validation_error('Timestamp must be in the future for countdown mode') if seconds <= 0
parsed_time = Time.zone.now + timestamp_seconds params[:timestamp] = Time.zone.now + seconds
params[:timestamp] = parsed_time
params[:timer_mode] = 'countdown' params[:timer_mode] = 'countdown'
elsif timestamp.present? rescue ArgumentError
begin render_timer_validation_error('Invalid seconds format')
end
def validate_timestamp_param(timestamp, timer_mode)
parsed_time = Time.zone.parse(timestamp) parsed_time = Time.zone.parse(timestamp)
if parsed_time.nil? if parsed_time.nil?
return render_timer_validation_error('Invalid timestamp format') return render_timer_validation_error('Invalid timestamp format')
@ -476,17 +492,7 @@ class TournamentsController < ApplicationController
params[:timestamp] = parsed_time params[:timestamp] = parsed_time
params[:timer_mode] = timer_mode.presence || 'countdown' params[:timer_mode] = timer_mode.presence || 'countdown'
rescue ArgumentError rescue ArgumentError
return render_timer_validation_error('Invalid timestamp format') 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')
end
end end
def normalize_timer_reason_params def normalize_timer_reason_params
@ -518,17 +524,13 @@ class TournamentsController < ApplicationController
timer_reason_param_present, timer_reason_param_present,
timer_reason_text_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' if timer_reason_text_param_present && !timer_reason_param_present
return 'timer_reason is required when timer_reason_text is provided'
end
return if timer_reason.blank? && timer_reason_text.blank? 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 '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) 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' if timer_reason == 'custom' && timer_reason_text.blank?
return 'timer_reason_text is required for custom timer_reason'
end
return unless timer_reason != 'custom' && timer_reason_text.present? return unless timer_reason != 'custom' && timer_reason_text.present?

View File

@ -17,7 +17,7 @@ class AdvanceTeamsInIntermediateStage
# populate stage below with the "winners" from single team matches # populate stage below with the "winners" from single team matches
matches.select { |m| m.state == 'single_team' } matches.select { |m| m.state == 'single_team' }
.each do |match| .each do |match|
context.fail! unless PopulateMatchBelowAndSave.call(match: match).success? context.fail! unless PopulateMatchBelowAndSave.call(match:).success?
end end
(context.object_to_save ||= []) << intermediate_stage (context.object_to_save ||= []) << intermediate_stage
end end

View File

@ -10,9 +10,9 @@ class Stage < ApplicationRecord
delegate :owner, to: :tournament delegate :owner, to: :tournament
def teams def teams
if !matches.size.zero? if !matches.empty?
matches.map(&:teams).flatten.uniq matches.map(&:teams).flatten.uniq
elsif !groups.size.zero? elsif !groups.empty?
groups.map(&:teams).flatten.uniq groups.map(&:teams).flatten.uniq
else else
[] []
@ -20,9 +20,9 @@ class Stage < ApplicationRecord
end end
def over? 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?) relevant_matches = groups.map(&:matches).flatten.reject(&:decider?)
return relevant_matches.find { |m| m.state != 'finished' }.nil? return relevant_matches.find { |m| m.state != 'finished' }.nil?
end end

View File

@ -6,7 +6,8 @@ class TeamActionList < ApplicationRecord
validates :name, :key, :action_name, :source, presence: true validates :name, :key, :action_name, :source, presence: true
validates :key, uniqueness: { scope: :tournament_id } 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 delegate :owner, to: :tournament
@ -20,7 +21,7 @@ class TeamActionList < ApplicationRecord
team_action_items.where.not(team_id: desired_team_ids).destroy_all team_action_items.where.not(team_id: desired_team_ids).destroy_all
desired_team_ids.each do |team_id| 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 end
end end

View File

@ -9,7 +9,7 @@ class Tournament < ApplicationRecord
METADATA_STRING_ATTRIBUTES = %i[location website_url].freeze METADATA_STRING_ATTRIBUTES = %i[location website_url].freeze
belongs_to :user 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 :teams, dependent: :destroy
has_many :stages, dependent: :destroy has_many :stages, dependent: :destroy
has_many :beamers, dependent: :destroy has_many :beamers, dependent: :destroy

View File

@ -45,9 +45,9 @@ class TournamentSyncQueueEntry < ApplicationRecord
def acquire_lock! def acquire_lock!
token = SecureRandom.hex(8) token = SecureRandom.hex(8)
updated = self.class updated = self.class
.where(id: id) .where(id:)
.where(locked_at: nil) .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) .update_all(locked_at: Time.current, lock_token: token)
return nil if updated.zero? return nil if updated.zero?

View File

@ -3,18 +3,18 @@
class GroupStageService class GroupStageService
class << self class << self
def generate_group_stage(groups) 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 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? 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 } 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 end
def get_group_object_from(team_array) def get_group_object_from(team_array)
Group.new matches: generate_all_matches_between(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 end
def deal_with_spacing_of_teams(matches, team_size) def deal_with_spacing_of_teams(matches, team_size)
@ -71,7 +71,7 @@ class GroupStageService
def update_group_scores(group) def update_group_scores(group)
changed_group_scores = [] changed_group_scores = []
group.teams.each do |team| 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 } matches = group.matches.select { |match| match.teams.include? team }
# reset previous values # reset previous values
group_score.group_points = 0 group_score.group_points = 0
@ -239,11 +239,9 @@ class GroupStageService
start_index -= 1 while start_index.positive? && sorted_group_scores[start_index - 1].position == position start_index -= 1 while start_index.positive? && sorted_group_scores[start_index - 1].position == position
end_index = index end_index = index
while end_index < sorted_group_scores.size - 1 && sorted_group_scores[end_index + 1].position == position end_index += 1 while end_index < sorted_group_scores.size - 1 && sorted_group_scores[end_index + 1].position == position
end_index += 1
end
{ start_index: start_index, end_index: end_index } { start_index:, end_index: }
end end
def advancing_slots_for_group(group) def advancing_slots_for_group(group)
@ -272,7 +270,7 @@ class GroupStageService
advancing_slots = advancing_slots_for_group(group) advancing_slots = advancing_slots_for_group(group)
group_ready_for_decider_check = regular_group_matches_finished?(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 next if sorted_group_scores.size <= 1
comparison_index = comparison_partner_index(sorted_group_scores, index) comparison_index = comparison_partner_index(sorted_group_scores, index)
@ -297,7 +295,8 @@ class GroupStageService
tied: comparison_reason[:tied], tied: comparison_reason[:tied],
needs_decider_match: group_ready_for_decider_check && needs_decider_match: group_ready_for_decider_check &&
comparison_reason[:tied] && comparison_reason[:tied] &&
unresolved_tie_affects_advancing_placements?(sorted_group_scores, group_score, advancing_slots) unresolved_tie_affects_advancing_placements?(sorted_group_scores, group_score,
advancing_slots)
} }
end end

View File

@ -16,7 +16,7 @@ class PlayoffStageService
initial_stage.state = :intermediate_stage unless initial_stage.matches.find(&:single_team?).nil? initial_stage.state = :intermediate_stage unless initial_stage.matches.find(&:single_team?).nil?
playoffs << initial_stage playoffs << initial_stage
# empty stages are the stages, the tournament is filled with to have the matches ready for later # 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.concat empty_stages
playoffs playoffs
end end
@ -25,7 +25,7 @@ class PlayoffStageService
# #
# @param stage_count [Integer] number of stages to generate # @param stage_count [Integer] number of stages to generate
# @return [Array] the generated stages # @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 = [] empty_stages = []
stage_count.times do |i| stage_count.times do |i|
match_count = 2**i match_count = 2**i
@ -82,7 +82,9 @@ class PlayoffStageService
# of which the winners advance into the match below # 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 # 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) 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) winners = get_winners_of(companion_match, current_match)
populate_match!(match: next_stage.matches.find { |m| m.position == current_match.position / 2 }, teams: winners) populate_match!(match: next_stage.matches.find { |m| m.position == current_match.position / 2 }, teams: winners)

View File

@ -3,7 +3,7 @@
class TeamActionItemUpdate class TeamActionItemUpdate
class << self class << self
def call(team_action_item:, completed:) def call(team_action_item:, completed:)
new(team_action_item: team_action_item, completed: completed).call new(team_action_item:, completed:).call
end end
end end
@ -13,7 +13,7 @@ class TeamActionItemUpdate
end end
def call 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) TournamentSyncEnqueue.call(team_action_item.tournament)
true true

View File

@ -23,9 +23,7 @@ class TournamentPlayoffDeletion
playoff_stages = tournament.stages.reload.select { |stage| stage.level >= 0 } playoff_stages = tournament.stages.reload.select { |stage| stage.level >= 0 }
return failure('Playoffs have not been generated') if playoff_stages.empty? 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') unless playoffs_deletable?(playoff_stages)
return failure('Playoffs cannot be deleted after matches have started or scores changed')
end
playoff_stages.each(&:destroy!) playoff_stages.each(&:destroy!)
group_stage.update!(state: :in_progress) group_stage.update!(state: :in_progress)

View File

@ -14,7 +14,7 @@ class TournamentSnapshotImporter
].freeze ].freeze
def self.import!(tournament:, snapshot:) def self.import!(tournament:, snapshot:)
new(tournament: tournament, snapshot: snapshot).import! new(tournament:, snapshot:).import!
end end
def initialize(tournament:, snapshot:) 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 = existing_records.reject { |record| source_ids.include?(record.sync_source_id) }
stale_records.each(&:destroy!) stale_records.each(&:destroy!)
end end
def parse_time(value) def parse_time(value)
return nil if value.blank? return nil if value.blank?
@ -242,6 +243,6 @@ class TournamentSnapshotImporter
def code_taken_by_another_tournament?(code) def code_taken_by_another_tournament?(code)
return false if code.blank? 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
end end

View File

@ -13,7 +13,7 @@ class TournamentSyncEnqueue
return false unless tournament.sync_push_enabled? return false unless tournament.sync_push_enabled?
payload = TournamentSnapshotBuilder.build(fresh_tournament) 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.snapshot_json = JSON.dump(payload)
entry.status = 'pending' entry.status = 'pending'
entry.attempts = 0 entry.attempts = 0

View File

@ -27,11 +27,9 @@ class TournamentSyncPusher
end end
def push_snapshot!(snapshot) 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}" unless response.is_a?(Net::HTTPSuccess)
raise SyncFailed, "sync push failed with status #{response.code}: #{response.body}"
end
tournament.update_columns(sync_last_pushed_at: Time.current, sync_last_push_error: nil) tournament.update_columns(sync_last_pushed_at: Time.current, sync_last_push_error: nil)
true true
@ -53,7 +51,7 @@ class TournamentSyncPusher
request['accept'] = 'application/json' request['accept'] = 'application/json'
request['content-type'] = 'application/json' request['content-type'] = 'application/json'
request['authorization'] = "Bearer #{tournament.sync_auth_token}" 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| Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
timeout = http_timeout_seconds timeout = http_timeout_seconds

View File

@ -5,9 +5,7 @@ class TournamentTransactionLogger
def lock_tournament!(tournament) def lock_tournament!(tournament)
raise ArgumentError, 'tournament is required' if tournament.nil? raise ArgumentError, 'tournament is required' if tournament.nil?
unless ActiveRecord::Base.connection.transaction_open? raise 'Tournament transaction log writes must happen inside a transaction' unless ActiveRecord::Base.connection.transaction_open?
raise 'Tournament transaction log writes must happen inside a transaction'
end
# Lock a fresh row instance so callers keep saved_change_to_* state for # Lock a fresh row instance so callers keep saved_change_to_* state for
# after_commit callbacks on the object they just updated. # after_commit callbacks on the object they just updated.

View File

@ -7,7 +7,7 @@ class UserService
def bet!(match, team) def bet!(match, team)
validate_bet! match, team validate_bet! match, team
@user.bets.create! match: match, team: team @user.bets.create! match:, team:
end end
private private

View File

@ -50,6 +50,5 @@ module TurniereBackend
config.action_mailer.default_options = { config.action_mailer.default_options = {
from: 'noreply@turnie.re' from: 'noreply@turnie.re'
} }
end end
end end

View File

@ -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. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
require "bootsnap/setup" # Speed up boot time by caching expensive operations.
require 'bundler/setup' # Set up gems listed in the Gemfile.
require 'bootsnap/setup' # Speed up boot time by caching expensive operations.

View File

@ -16,7 +16,7 @@ Rails.application.configure do
# Enable/disable caching. By default caching is disabled. # Enable/disable caching. By default caching is disabled.
# Run rails dev:cache to toggle caching. # 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.action_controller.perform_caching = true
config.cache_store = :memory_store config.cache_store = :memory_store

View File

@ -61,7 +61,7 @@ Rails.application.configure do
config.active_support.deprecation = :notify config.active_support.deprecation = :notify
# Use default logging formatter so that PID and timestamp are not suppressed. # 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. # Use a different logger for distributed setups.
# require 'syslog/logger' # require 'syslog/logger'
@ -91,5 +91,5 @@ Rails.application.configure do
end end
# Logging to STDOUT (running in docker by default) # Logging to STDOUT (running in docker by default)
config.logger = Logger.new(STDOUT) config.logger = Logger.new($stdout)
end end

View File

@ -14,14 +14,14 @@ module DeviseRoutesRails8Compat
def devise_password(mapping, controllers) def devise_password(mapping, controllers)
resource :password, resource :password,
only: [:new, :create, :edit, :update], only: %i[new create edit update],
path: mapping.path_names[:password], path: mapping.path_names[:password],
controller: controllers[:passwords] controller: controllers[:passwords]
end end
def devise_confirmation(mapping, controllers) def devise_confirmation(mapping, controllers)
resource :confirmation, resource :confirmation,
only: [:new, :create, :show], only: %i[new create show],
path: mapping.path_names[:confirmation], path: mapping.path_names[:confirmation],
controller: controllers[:confirmations] controller: controllers[:confirmations]
end end
@ -30,14 +30,14 @@ module DeviseRoutesRails8Compat
return unless mapping.to.unlock_strategy_enabled?(:email) return unless mapping.to.unlock_strategy_enabled?(:email)
resource :unlock, resource :unlock,
only: [:new, :create, :show], only: %i[new create show],
path: mapping.path_names[:unlock], path: mapping.path_names[:unlock],
controller: controllers[:unlocks] controller: controllers[:unlocks]
end end
def devise_registration(mapping, controllers) def devise_registration(mapping, controllers)
resource :registration, resource :registration,
only: [:new, :create, :edit, :update, :destroy], only: %i[new create edit update destroy],
path: mapping.path_names[:registration], path: mapping.path_names[:registration],
path_names: { path_names: {
new: mapping.path_names[:sign_up], new: mapping.path_names[:sign_up],

View File

@ -1,8 +1,10 @@
# frozen_string_literal: true
# Be sure to restart your server when you modify this file. # 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 # Configure parameters to be filtered from the log file. Use this to limit dissemination of
# sensitive information. See the ActiveSupport::ParameterFilter documentation for supported # sensitive information. See the ActiveSupport::ParameterFilter documentation for supported
# notations and behaviors. # notations and behaviors.
Rails.application.config.filter_parameters += [ Rails.application.config.filter_parameters += %i[
:passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn passw secret token _key crypt salt certificate otp ssn
] ]

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# Be sure to restart your server when you modify this file. # Be sure to restart your server when you modify this file.
# #
# This file eases your Rails 7.0 framework defaults upgrade. # This file eases your Rails 7.0 framework defaults upgrade.

View File

@ -8,10 +8,9 @@
].each { |path| Spring.watch(path) } ].each { |path| Spring.watch(path) }
Spring.after_fork do Spring.after_fork do
if ENV['DEBUGGER_STORED_RUBYLIB'] ENV['DEBUGGER_STORED_RUBYLIB']&.split(File::PATH_SEPARATOR)&.each do |path|
ENV['DEBUGGER_STORED_RUBYLIB'].split(File::PATH_SEPARATOR).each do |path| next unless path.include?('ruby-debug-ide')
next unless path =~ /ruby-debug-ide/
load path + '/ruby-debug-ide/multiprocess/starter.rb' load "#{path}/ruby-debug-ide/multiprocess/starter.rb"
end
end end
end end

View File

@ -1,18 +1,20 @@
# frozen_string_literal: true
# This migration comes from active_storage (originally 20190112182829) # This migration comes from active_storage (originally 20190112182829)
class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0] class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0]
def up def up
return unless table_exists?(:active_storage_blobs) return unless table_exists?(:active_storage_blobs)
unless column_exists?(:active_storage_blobs, :service_name) return if column_exists?(:active_storage_blobs, :service_name)
add_column :active_storage_blobs, :service_name, :string add_column :active_storage_blobs, :service_name, :string
if configured_service = ActiveStorage::Blob.service.name if (configured_service = ActiveStorage::Blob.service.name)
ActiveStorage::Blob.unscoped.update_all(service_name: configured_service) ActiveStorage::Blob.unscoped.update_all(service_name: configured_service)
end end
change_column :active_storage_blobs, :service_name, :string, null: false change_column :active_storage_blobs, :service_name, :string, null: false
end end
end
def down def down
return unless table_exists?(:active_storage_blobs) return unless table_exists?(:active_storage_blobs)

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# This migration comes from active_storage (originally 20191206030411) # This migration comes from active_storage (originally 20191206030411)
class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0] class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0]
def change def change
@ -8,12 +10,13 @@ class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0]
t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type
t.string :variation_digest, null: false 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 t.foreign_key :active_storage_blobs, column: :blob_id
end end
end end
private private
def primary_key_type def primary_key_type
config = Rails.configuration.generators config = Rails.configuration.generators
config.options[config.orm][:primary_key_type] || :primary_key config.options[config.orm][:primary_key_type] || :primary_key

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
# This migration comes from active_storage (originally 20211119233751) # This migration comes from active_storage (originally 20211119233751)
class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0] class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0]
def change def change

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class AddPositionToGroupScores < ActiveRecord::Migration[7.0] class AddPositionToGroupScores < ActiveRecord::Migration[7.0]
def change def change
add_column :group_scores, :position, :integer add_column :group_scores, :position, :integer

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class AddDefaultToPositionInGroupScores < ActiveRecord::Migration[7.0] class AddDefaultToPositionInGroupScores < ActiveRecord::Migration[7.0]
def change def change
change_column_default :group_scores, :position, 0 change_column_default :group_scores, :position, 0

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class AddTimerEndToTournaments < ActiveRecord::Migration[7.0] class AddTimerEndToTournaments < ActiveRecord::Migration[7.0]
def change def change
add_column :tournaments, :timer_end, :datetime add_column :tournaments, :timer_end, :datetime

View File

@ -13,7 +13,7 @@ class CreateTeamActionLists < ActiveRecord::Migration[7.0]
t.timestamps t.timestamps
end 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| create_table :team_action_items do |t|
t.references :team_action_list, null: false, foreign_key: { on_delete: :cascade }, index: true 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 t.timestamps
end 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
end end

View File

@ -1,3 +1,5 @@
# frozen_string_literal: true
class AddTimerModeToTournaments < ActiveRecord::Migration[7.0] class AddTimerModeToTournaments < ActiveRecord::Migration[7.0]
def change def change
add_column :tournaments, :timer_mode, :string add_column :tournaments, :timer_mode, :string

View File

@ -8,9 +8,7 @@ class AddSyncAuthTokenDigestToTournaments < ActiveRecord::Migration[7.0]
end end
def up def up
unless column_exists?(:tournaments, :sync_auth_token_digest) add_column :tournaments, :sync_auth_token_digest, :string unless column_exists?(:tournaments, :sync_auth_token_digest)
add_column :tournaments, :sync_auth_token_digest, :string
end
MigrationTournament.reset_column_information MigrationTournament.reset_column_information
MigrationTournament MigrationTournament

View File

@ -14,7 +14,7 @@ class CreateTournamentTransactionLogEntries < ActiveRecord::Migration[7.0]
end end
add_index :tournament_transaction_log_entries, add_index :tournament_transaction_log_entries,
[:tournament_id, :sequence], %i[tournament_id sequence],
unique: true, unique: true,
name: 'index_tournament_log_entries_on_tournament_and_sequence' name: 'index_tournament_log_entries_on_tournament_and_sequence'
end end

View File

@ -11,7 +11,7 @@ module TurniereE2E
ACTION_CABLE_PROTOCOLS = %w[actioncable-v1-json actioncable-unsupported].freeze ACTION_CABLE_PROTOCOLS = %w[actioncable-v1-json actioncable-unsupported].freeze
def initialize(base_url:) def initialize(base_url:)
@base_url = base_url.sub(%r{/\z}, '') @base_url = base_url.delete_suffix('/')
@messages = Queue.new @messages = Queue.new
@closed = false @closed = false
end end
@ -19,7 +19,7 @@ module TurniereE2E
def subscribe_team_action_lists!(tournament_id:) def subscribe_team_action_lists!(tournament_id:)
identifier = { identifier = {
channel: 'TournamentTeamActionListsChannel', channel: 'TournamentTeamActionListsChannel',
tournament_id: tournament_id tournament_id:
}.to_json }.to_json
subscribe!(identifier:) do |message| subscribe!(identifier:) do |message|
@ -30,7 +30,7 @@ module TurniereE2E
def subscribe_timer!(tournament_id:) def subscribe_timer!(tournament_id:)
identifier = { identifier = {
channel: 'TournamentTimersChannel', channel: 'TournamentTimersChannel',
tournament_id: tournament_id tournament_id:
}.to_json }.to_json
subscribe!(identifier:) do |message| subscribe!(identifier:) do |message|
@ -41,8 +41,8 @@ module TurniereE2E
def subscribe_beamer!(tournament_id:, beamer_name:) def subscribe_beamer!(tournament_id:, beamer_name:)
identifier = { identifier = {
channel: 'TournamentBeamersChannel', channel: 'TournamentBeamersChannel',
tournament_id: tournament_id, tournament_id:,
beamer_name: beamer_name beamer_name:
}.to_json }.to_json
subscribe!(identifier:) do |message| subscribe!(identifier:) do |message|
@ -78,31 +78,31 @@ module TurniereE2E
end end
def wait_for_payload!(timeout: 20) def wait_for_payload!(timeout: 20)
wait_for!(timeout: timeout) do |message| wait_for!(timeout:) do |message|
team_action_lists_payload_message?(message) team_action_lists_payload_message?(message)
end.fetch('message') end.fetch('message')
end end
def wait_for_timer_payload!(timeout: 20) def wait_for_timer_payload!(timeout: 20)
wait_for!(timeout: timeout) do |message| wait_for!(timeout:) do |message|
timer_payload_message?(message) timer_payload_message?(message)
end.fetch('message') end.fetch('message')
end end
def wait_for_beamer_payload!(timeout: 20) def wait_for_beamer_payload!(timeout: 20)
wait_for!(timeout: timeout) do |message| wait_for!(timeout:) do |message|
beamer_payload_message?(message) beamer_payload_message?(message)
end.fetch('message') end.fetch('message')
end end
def wait_for_tournament_payload!(timeout: 20) def wait_for_tournament_payload!(timeout: 20)
wait_for!(timeout: timeout) do |message| wait_for!(timeout:) do |message|
tournament_payload_message?(message) tournament_payload_message?(message)
end.fetch('message') end.fetch('message')
end end
def wait_for_matches_payload!(timeout: 20) def wait_for_matches_payload!(timeout: 20)
wait_for!(timeout: timeout) do |message| wait_for!(timeout:) do |message|
matches_payload_message?(message) matches_payload_message?(message)
end.fetch('message') end.fetch('message')
end end
@ -135,7 +135,7 @@ module TurniereE2E
driver.text( driver.text(
JSON.dump( JSON.dump(
command: 'subscribe', command: 'subscribe',
identifier: identifier identifier:
) )
) )
@ -184,8 +184,8 @@ module TurniereE2E
end end
def pump_until_open!(timeout: 10) def pump_until_open!(timeout: 10)
deadline = Time.now + timeout deadline = monotonic_deadline(timeout)
while Time.now < deadline while Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline
read_from_socket(timeout: 0.5) read_from_socket(timeout: 0.5)
return if driver.ready_state == 1 return if driver.ready_state == 1
end end
@ -194,14 +194,14 @@ module TurniereE2E
end end
def wait_for!(timeout: 20) def wait_for!(timeout: 20)
deadline = Time.now + timeout deadline = monotonic_deadline(timeout)
loop do loop do
until @messages.empty? until @messages.empty?
message = @messages.pop(true) message = @messages.pop(true)
return message if yield(message) return message if yield(message)
end 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) read_from_socket(timeout: 0.5)
rescue ThreadError rescue ThreadError
@ -209,6 +209,10 @@ module TurniereE2E
end end
end end
def monotonic_deadline(timeout)
Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
end
def read_from_socket(timeout:) def read_from_socket(timeout:)
readable, = IO.select([socket], nil, nil, timeout) readable, = IO.select([socket], nil, nil, timeout)
return if readable.nil? return if readable.nil?

View File

@ -19,7 +19,7 @@ module TurniereE2E
attr_reader :base_url attr_reader :base_url
def initialize(base_url:) def initialize(base_url:)
@base_url = base_url.sub(%r{/\z}, '') @base_url = base_url.delete_suffix('/')
@auth_headers = {} @auth_headers = {}
@login_credentials = nil @login_credentials = nil
end end
@ -29,10 +29,10 @@ module TurniereE2E
:post, :post,
'/users', '/users',
body: { body: {
email: email, email:,
password: password, password:,
password_confirmation: password, password_confirmation: password,
username: username username:
} }
) )
store_auth_headers!(response) store_auth_headers!(response)
@ -40,14 +40,14 @@ module TurniereE2E
end end
def login!(email:, password:) def login!(email:, password:)
@login_credentials = { email: email, password: password } @login_credentials = { email:, password: }
@auth_headers = {} @auth_headers = {}
response = request( response = request(
:post, :post,
'/users/sign_in', '/users/sign_in',
body: { body: {
email: email, email:,
password: password password:
} }
) )
store_auth_headers!(response) store_auth_headers!(response)
@ -59,19 +59,19 @@ module TurniereE2E
end end
def get(path, params: nil, headers: {}) def get(path, params: nil, headers: {})
request(:get, path, params: params, headers: headers) request(:get, path, params:, headers:)
end end
def post(path, body: nil, params: nil, headers: {}) def post(path, body: nil, params: nil, headers: {})
request(:post, path, body: body, params: params, headers: headers) request(:post, path, body:, params:, headers:)
end end
def patch(path, body: nil, params: nil, headers: {}) def patch(path, body: nil, params: nil, headers: {})
request(:patch, path, body: body, params: params, headers: headers) request(:patch, path, body:, params:, headers:)
end end
def delete(path, params: nil, headers: {}) def delete(path, params: nil, headers: {})
request(:delete, path, params: params, headers: headers) request(:delete, path, params:, headers:)
end end
private private
@ -80,7 +80,7 @@ module TurniereE2E
login!(**@login_credentials) if refresh_session && should_refresh_session?(method, path) login!(**@login_credentials) if refresh_session && should_refresh_session?(method, path)
uri = URI.parse("#{base_url}#{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 request_class = case method
when :get then Net::HTTP::Get when :get then Net::HTTP::Get
@ -108,7 +108,8 @@ module TurniereE2E
store_auth_headers!(result) store_auth_headers!(result)
if retry_on_unauthorized && result[:status] == 401 && can_reauthenticate?(path) if retry_on_unauthorized && result[:status] == 401 && can_reauthenticate?(path)
login!(**@login_credentials) 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 end
result result
end end

View File

@ -267,12 +267,8 @@ module TurniereE2E
def websocket_failures(level) def websocket_failures(level)
failures = [] failures = []
if level.fetch(:errors).positive? failures << "WS #{level.fetch(:clients)} clients: #{level.fetch(:errors)} failed" if level.fetch(:errors).positive?
failures << "WS #{level.fetch(:clients)} clients: #{level.fetch(:errors)} failed" failures << "WS #{level.fetch(:clients)} clients: only #{level.fetch(:opened)} opened" if websocket_open_short?(level)
end
if websocket_open_short?(level)
failures << "WS #{level.fetch(:clients)} clients: only #{level.fetch(:opened)} opened"
end
if websocket_metric_short?(level) if websocket_metric_short?(level)
failures << "WS #{level.fetch(:clients)} clients: metrics saw #{level.fetch(:connection_metric)} connections" failures << "WS #{level.fetch(:clients)} clients: metrics saw #{level.fetch(:connection_metric)} connections"
end end

View File

@ -7,7 +7,7 @@ require 'uri'
module TurniereE2E module TurniereE2E
class ScenarioRunner class ScenarioRunner
DEFAULT_PASSWORD = 'password123'.freeze DEFAULT_PASSWORD = 'password123'
attr_reader :client attr_reader :client
@ -36,19 +36,19 @@ module TurniereE2E
timestamp: timer_response.fetch(:timestamp), timestamp: timer_response.fetch(:timestamp),
timer_reason: timer_response.fetch(:timer_reason) 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!( upcoming_matches = get!(
"/tournaments/#{tournament.fetch(:id)}/matches", "/tournaments/#{tournament.fetch(:id)}/matches",
params: { state: 'upcoming' } params: { state: 'upcoming' }
) )
checkpoints[:upcoming_group_matches] = { match_ids: upcoming_matches.map { |match| match.fetch(:id) } } 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) finish_group_stage_matches!(tournament)
tournament = fetch_tournament!(tournament.fetch(:id)) tournament = fetch_tournament!(tournament.fetch(:id))
checkpoints[:groups_finished] = summarize_tournament(tournament) 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") statistics = get!("/tournaments/#{tournament.fetch(:id)}/statistics")
checkpoints[:statistics] = { checkpoints[:statistics] = {
@ -60,12 +60,12 @@ module TurniereE2E
finish_stage!(group_stage_of!(tournament).fetch(:id)) finish_stage!(group_stage_of!(tournament).fetch(:id))
tournament = fetch_tournament!(tournament.fetch(:id)) tournament = fetch_tournament!(tournament.fetch(:id))
checkpoints[:playoffs_created] = summarize_tournament(tournament) 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)) run_playoffs_until_before_final!(tournament.fetch(:id))
tournament = fetch_tournament!(tournament.fetch(:id)) tournament = fetch_tournament!(tournament.fetch(:id))
checkpoints[:before_final_finish_attempt] = summarize_tournament(tournament) 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) finish_attempt = finish_final_stage_matches!(tournament)
tournament = fetch_tournament!(tournament.fetch(:id)) tournament = fetch_tournament!(tournament.fetch(:id))
@ -86,18 +86,27 @@ module TurniereE2E
checkpoints = { checkpoints = {
created: summarize_tournament(tournament) 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_group_stage_matches!(tournament)
finish_stage!(group_stage_of!(fetch_tournament!(tournament.fetch(:id))).fetch(:id)) finish_stage!(group_stage_of!(fetch_tournament!(tournament.fetch(:id))).fetch(:id))
tournament = fetch_tournament!(tournament.fetch(:id)) tournament = fetch_tournament!(tournament.fetch(:id))
checkpoints[:playoffs_created] = summarize_tournament(tournament) 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)) run_playoffs_until_before_final!(tournament.fetch(:id))
tournament = fetch_tournament!(tournament.fetch(:id)) tournament = fetch_tournament!(tournament.fetch(:id))
checkpoints[:before_final_finish_attempt] = summarize_tournament(tournament) 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) finish_attempt = finish_final_stage_matches!(tournament)
tournament = fetch_tournament!(tournament.fetch(:id)) tournament = fetch_tournament!(tournament.fetch(:id))
@ -132,7 +141,10 @@ module TurniereE2E
checkpoints[created_key] = summarize_tournament(tournament).merge( checkpoints[created_key] = summarize_tournament(tournament).merge(
playoff_matchups: playoff_matchups(tournament) 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 next if index == rounds.size - 1
@ -140,7 +152,10 @@ module TurniereE2E
tournament = fetch_tournament!(tournament.fetch(:id)) tournament = fetch_tournament!(tournament.fetch(:id))
deleted_key = deleted_keys.fetch(index) deleted_key = deleted_keys.fetch(index)
checkpoints[deleted_key] = summarize_tournament(tournament) 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 end
result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at)
@ -152,7 +167,10 @@ module TurniereE2E
checkpoints = { checkpoints = {
created: summarize_tournament(tournament) 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)) run_playoffs_until_complete!(tournament.fetch(:id))
tournament = fetch_tournament!(tournament.fetch(:id)) tournament = fetch_tournament!(tournament.fetch(:id))
@ -170,7 +188,10 @@ module TurniereE2E
checkpoints = { checkpoints = {
created: summarize_tournament(tournament) 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 group = group_stage_of!(tournament).fetch(:groups).first
play_group_matches_with_explicit_scores!(group, group_direct_comparison_score_plan(case_name)) play_group_matches_with_explicit_scores!(group, group_direct_comparison_score_plan(case_name))
@ -250,7 +271,10 @@ module TurniereE2E
checkpoints = { checkpoints = {
created: summarize_tournament(tournament) 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)) profiled_show = fetch_tournament_profile!(tournament.fetch(:id))
checkpoints[:profiled_show] = profiled_show checkpoints[:profiled_show] = profiled_show
@ -259,7 +283,7 @@ module TurniereE2E
end end
def wait_for_healthcheck!(timeout_seconds: 30) def wait_for_healthcheck!(timeout_seconds: 30)
started_at = Time.now started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
loop do loop do
begin begin
response = client.get('/healthz') response = client.get('/healthz')
@ -268,7 +292,8 @@ module TurniereE2E
nil nil
end 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 sleep 0.5
end end
@ -282,7 +307,7 @@ module TurniereE2E
unique = SecureRandom.hex(6) unique = SecureRandom.hex(6)
email = "e2e-#{unique}@example.com" email = "e2e-#{unique}@example.com"
username = "e2e-#{unique}" 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? return { email: email, username: username } if client.authenticated?
login_response = client.login!(email: email, password: DEFAULT_PASSWORD) login_response = client.login!(email: email, password: DEFAULT_PASSWORD)
@ -350,9 +375,12 @@ module TurniereE2E
def run_playoffs_until_before_final!(tournament_id) def run_playoffs_until_before_final!(tournament_id)
loop do loop do
tournament = fetch_tournament!(tournament_id) tournament = fetch_tournament!(tournament_id)
playable_matches = playoff_matches(tournament).reject { |match| match.fetch(:stage_level) == 0 } playable_matches = playoff_matches(tournament).reject { |match| match.fetch(:stage_level).zero? }
.select { |match| match.fetch(:state) == 'not_started' } .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? break if playable_matches.empty?
@ -366,7 +394,10 @@ module TurniereE2E
loop do loop do
tournament = fetch_tournament!(tournament_id) tournament = fetch_tournament!(tournament_id)
playable_matches = playoff_matches(tournament).select { |match| match.fetch(:state) == 'not_started' } 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? break if playable_matches.empty?
playable_matches.each do |match| playable_matches.each do |match|
@ -390,7 +421,9 @@ module TurniereE2E
expect_status!(finished, expected_finish_status, "finish match #{match.fetch(:id)}") expect_status!(finished, expected_finish_status, "finish match #{match.fetch(:id)}")
if expected_finish_error if expected_finish_error
actual_error = finished.fetch(:json).fetch(: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 end
{ {
@ -418,7 +451,9 @@ module TurniereE2E
expect_status!(finished, expected_finish_status, "finish match #{match.fetch(:id)}") expect_status!(finished, expected_finish_status, "finish match #{match.fetch(:id)}")
if expected_finish_error if expected_finish_error
actual_error = finished.fetch(:json).fetch(: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 end
{ {
@ -481,7 +516,9 @@ module TurniereE2E
end end
def final_stage_matches_of!(tournament) 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) } attach_stage_level(final_stage).fetch(:matches).sort_by { |match| match.fetch(:position) }
end end
@ -496,7 +533,9 @@ module TurniereE2E
{ {
status: 200, 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 end
@ -574,7 +613,7 @@ module TurniereE2E
def create_group_stage_payload(group_count:, teams_per_group:, playoff_teams_amount:) def create_group_stage_payload(group_count:, teams_per_group:, playoff_teams_amount:)
teams = group_count.times.flat_map do |group_index| 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}", name: "G#{group_index + 1}-S#{seed_index + 1}",
group: group_index group: group_index
@ -626,7 +665,7 @@ module TurniereE2E
name: "HTTP E2E Playoff Only #{team_count} #{SecureRandom.hex(3)}", name: "HTTP E2E Playoff Only #{team_count} #{SecureRandom.hex(3)}",
description: 'HTTP lifecycle E2E tournament', description: 'HTTP lifecycle E2E tournament',
public: false, 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 end
@ -739,7 +778,7 @@ module TurniereE2E
end end
def parse_server_timing(header) 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| header.split(',').each_with_object({}) do |entry, sections|
name, duration = entry.strip.match(/\A([^;]+);dur=([0-9.]+)\z/)&.captures name, duration = entry.strip.match(/\A([^;]+);dur=([0-9.]+)\z/)&.captures
@ -758,6 +797,10 @@ module TurniereE2E
} }
end 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) def expect_status!(response, expected_status, action)
return if response.fetch(:status) == expected_status return if response.fetch(:status) == expected_status

View File

@ -73,9 +73,7 @@ result = runner.run
if options[:format] == 'json' if options[:format] == 'json'
puts JSON.pretty_generate(result) puts JSON.pretty_generate(result)
exit else
end
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| result.fetch(:http).each do |level|
@ -102,3 +100,4 @@ puts(
"action_cable_connections_total=#{metrics.fetch(:action_cable_connections_total)}, " \ "action_cable_connections_total=#{metrics.fetch(:action_cable_connections_total)}, " \
"ruby_threads=#{metrics.fetch(:ruby_threads)}" "ruby_threads=#{metrics.fetch(:ruby_threads)}"
) )
end

View File

@ -28,7 +28,7 @@ module Local
result = yield result = yield
end end
duration = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000.0 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 result
end end

29
script/e2e_scenarios.rb Normal file → Executable file
View File

@ -5,6 +5,11 @@ require 'json'
require 'optparse' require 'optparse'
require_relative '../e2e/lib/scenario_runner' require_relative '../e2e/lib/scenario_runner'
def fail_with(message)
warn message
raise SystemExit, 1
end
options = { options = {
base_url: ENV.fetch('TURNIERE_E2E_BASE_URL', 'http://127.0.0.1:3000'), base_url: ENV.fetch('TURNIERE_E2E_BASE_URL', 'http://127.0.0.1:3000'),
stop_at: nil, stop_at: nil,
@ -12,9 +17,9 @@ options = {
group_count: 4, group_count: 4,
teams_per_group: 4, teams_per_group: 4,
playoff_teams_amount: 8, playoff_teams_amount: 8,
email: ENV['TURNIERE_E2E_EMAIL'], email: ENV.fetch('TURNIERE_E2E_EMAIL', nil),
password: ENV['TURNIERE_E2E_PASSWORD'], password: ENV.fetch('TURNIERE_E2E_PASSWORD', nil),
username: ENV['TURNIERE_E2E_USERNAME'] username: ENV.fetch('TURNIERE_E2E_USERNAME', nil)
} }
OptionParser.new do |parser| 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('--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('--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('--team-count COUNT', Integer, 'Team count for playoff_only scenario') do |value|
parser.on('--group-count COUNT', Integer, 'Group count for group-stage scenarios') { |value| options[:group_count] = value } options[:team_count] = value
parser.on('--teams-per-group COUNT', Integer, 'Teams per group for group-stage scenarios') { |value| options[:teams_per_group] = 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| parser.on('--playoff-teams-amount COUNT', Integer, 'Playoff teams amount for group-stage scenarios') do |value|
options[:playoff_teams_amount] = value options[:playoff_teams_amount] = value
end end
@ -33,7 +44,9 @@ OptionParser.new do |parser|
parser.on('--username USERNAME', 'Confirmed E2E user username') { |value| options[:username] = value } parser.on('--username USERNAME', 'Confirmed E2E user username') { |value| options[:username] = value }
end.parse! 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( runner = TurniereE2E::ScenarioRunner.new(
base_url: options[:base_url], base_url: options[:base_url],
email: options[:email], email: options[:email],
@ -58,7 +71,7 @@ result = case scenario
stop_at: options[:stop_at] || :profiled_show stop_at: options[:stop_at] || :profiled_show
) )
else else
abort("Unknown scenario #{scenario.inspect}") fail_with("Unknown scenario #{scenario.inspect}")
end end
puts JSON.pretty_generate(result) puts JSON.pretty_generate(result)

0
script/wait_for_http.rb Normal file → Executable file
View File

View File

@ -57,6 +57,18 @@ BLACKBOX_MAILGUN_API_KEY = "blackbox-test-api-key"
BLACKBOX_MAILGUN_DOMAIN = "blackbox.example.com" BLACKBOX_MAILGUN_DOMAIN = "blackbox.example.com"
SPEC_ROOT = Path("spec") SPEC_ROOT = Path("spec")
E2E_SPEC_ROOT = SPEC_ROOT / "e2e" / "http" 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 ROOT = Path(__file__).resolve().parent
DB_DIR = ROOT / "db" DB_DIR = ROOT / "db"
GEMFILE_LOCK_PATH = ROOT / "Gemfile.lock" GEMFILE_LOCK_PATH = ROOT / "Gemfile.lock"
@ -802,7 +814,7 @@ def test_shard(c, node_index=1, node_total=8, dry_run=False):
@task @task
def lint(c): def lint(c):
"""Run RuboCop.""" """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") @task(name="bootstrap-dev-user")