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/**
.gitignore
.hound.yml
.rubocop.yml
docker-compose.yml
README.md
.gitlab-ci.yml

View File

@ -13,6 +13,21 @@ include:
- project: 'turniere/turniere-infra'
file: '/ci/pipeline.yaml'
lint:
stage: test
tags:
- docker
needs:
- job: build_image
image: $CI_REGISTRY_IMAGE/test/commits:$CI_COMMIT_SHA
rules:
- if: $CI_PIPELINE_SOURCE != "push"
when: never
- when: always
script:
- cd /app
- inv lint
rails spec:
stage: test
tags:

View File

@ -1,9 +1,3 @@
inherit_from: .rubocop_todo.yml
inherit_mode:
merge:
- Exclude
plugins:
- rubocop-performance
- rubocop-rails
@ -15,6 +9,7 @@ AllCops:
Exclude:
- "bin/**/*"
- "db/schema.rb"
- "spec/**/*.rb"
# Enable Rails
Rails:
@ -22,14 +17,14 @@ Rails:
# Commonly used screens these days easily fit more than 80 characters.
Layout/LineLength:
Max: 120
Max: 140
# Too short methods lead to extraction of single-use methods, which can make
# the code easier to read (by naming things), but can also clutter the class
Metrics/MethodLength:
Exclude:
- "db/migrate/*"
Max: 50
Max: 60
# The guiding principle of classes is SRP, SRP can't be accurately measured by LoC
Metrics/ClassLength:
@ -41,9 +36,10 @@ Metrics/ModuleLength:
Metrics/AbcSize:
Exclude:
- "db/migrate/*"
Max: 50
Max: 75
Metrics/BlockLength:
Max: 30
AllowedMethods:
- "namespace"
- "create_table"
@ -51,6 +47,42 @@ Metrics/BlockLength:
- "config/**/*.rb"
- "spec/**/*.rb"
Metrics/CyclomaticComplexity:
Max: 22
Metrics/ParameterLists:
Max: 7
Metrics/PerceivedComplexity:
Max: 24
Rails/ReversibleMigration:
Exclude:
- "db/migrate/**/*"
Rails/SkipsModelValidations:
Exclude:
- "app/models/tournament_sync_queue_entry.rb"
- "app/services/tournament_sync_pusher.rb"
- "db/migrate/**/*"
# These are standalone CLI entrypoints where stdout is command output,
# not Rails application logging.
Rails/Output:
Exclude:
- "e2e/load_test.rb"
- "script/e2e_scenarios.rb"
- "script/wait_for_http.rb"
Rails/Delegate:
Exclude:
- "app/controllers/tournaments_controller.rb"
- "e2e/lib/action_cable_client.rb"
Style/MultilineBlockChain:
Exclude:
- "e2e/lib/api_client.rb"
# Disable documentation checks for now
# should be done with yard instead
Style/Documentation:

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/environments/production.rb /app/config/environments/beta.rb
COPY tasks.py /app/tasks.py
COPY .rubocop.yml /app/.rubocop.yml
COPY e2e /app/e2e
COPY lib /app/lib
COPY spec /app/spec

View File

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

View File

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

View File

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

View File

@ -14,7 +14,8 @@ class TournamentsController < ApplicationController
before_action :set_tournament_for_show, only: %i[show]
before_action :set_tournament,
only: %i[update destroy destroy_playoffs set_timer timer sync_state test_sync start_next_group_matches]
before_action :authenticate_user!, only: %i[create update destroy destroy_playoffs set_timer test_sync start_next_group_matches]
before_action :authenticate_user!,
only: %i[create update destroy destroy_playoffs set_timer test_sync start_next_group_matches]
before_action -> { require_owner! @tournament.owner },
only: %i[update destroy destroy_playoffs set_timer test_sync start_next_group_matches]
before_action :validate_create_params, only: %i[create]
@ -74,6 +75,7 @@ class TournamentsController < ApplicationController
end
raise ActionController::ParameterMissing, :teams if teams.nil?
if group_stage
params.require(:playoff_teams_amount)
groups = organize_teams_in_groups(teams)
@ -339,7 +341,7 @@ class TournamentsController < ApplicationController
return if ActiveModel::Type::Boolean.new.cast(params[:read_only_mode])
teams = params['teams']
return if teams.is_a?(Array) && teams.reject { |t| t.is_a? ActionController::Parameters }.count.zero?
return if teams.is_a?(Array) && teams.count { |team| !team.is_a?(ActionController::Parameters) }.zero?
render json: { error: 'Invalid teams array' }, status: :unprocessable_content
end
@ -374,12 +376,16 @@ class TournamentsController < ApplicationController
error: 'playoff_teams_amount, instant_finalists_amount and intermediate_round_participants_amount don\'t match'
}, status: :unprocessable_content
end
def sync_state_params
params.require(:snapshot).permit!.to_h
end
def authorize_sync_request!
return render json: { error: 'Tournament is not configured to accept sync pushes' }, status: :unprocessable_content unless @tournament.sync_accepts_push?
unless @tournament.sync_accepts_push?
return render json: { error: 'Tournament is not configured to accept sync pushes' },
status: :unprocessable_content
end
token = request.authorization.to_s.delete_prefix('Bearer ').presence || request.headers['X-Tournament-Sync-Token'].to_s
return render json: { error: 'Missing sync token' }, status: :unauthorized if token.blank?
@ -425,13 +431,9 @@ class TournamentsController < ApplicationController
timer_reason_text = params[:timer_reason_text]
clear_timer = ActiveModel::Type::Boolean.new.cast(params[:clear_timer])
if clear_timer
return validate_clear_timer_params(timestamp, timestamp_seconds, timer_mode, timer_reason, timer_reason_text)
end
return validate_clear_timer_params(timestamp, timestamp_seconds, timer_mode, timer_reason, timer_reason_text) if clear_timer
if timer_mode.present? && !Tournament::TIMER_MODES.include?(timer_mode)
return render_timer_validation_error('Invalid timer_mode')
end
return render_timer_validation_error('Invalid timer_mode') if timer_mode.present? && !Tournament::TIMER_MODES.include?(timer_mode)
timer_reason_error = validate_timer_reason_params(
timer_reason,
@ -441,7 +443,20 @@ class TournamentsController < ApplicationController
)
return render_timer_validation_error(timer_reason_error) if timer_reason_error
# throw error if both timestamp and timestamp_seconds are present
return if validate_timer_timestamp_params(timestamp, timestamp_seconds, timer_mode)
if timer_mode == 'countup'
params[:timestamp] = Time.zone.now
elsif timer_mode.present?
render_timer_validation_error('timer_mode requires timestamp, timestamp_seconds, or clear_timer')
elsif timer_reason_update_requested
render_timer_validation_error('timer_reason requires active timer') unless timer_active?
else
render_timer_validation_error('timestamp, timestamp_seconds, or clear_timer is required')
end
end
def validate_timer_timestamp_params(timestamp, timestamp_seconds, timer_mode)
if timestamp.present? && timestamp_seconds.present?
return render_timer_validation_error('Only one of timestamp or timestamp_seconds is allowed')
end
@ -450,22 +465,23 @@ class TournamentsController < ApplicationController
return render_timer_validation_error('timestamp_seconds is only allowed for countdown mode')
end
if timestamp_seconds.present?
begin
timestamp_seconds = Integer(timestamp_seconds)
rescue ArgumentError
return render_timer_validation_error('Invalid seconds format')
return validate_timestamp_seconds_param(timestamp_seconds) if timestamp_seconds.present?
return validate_timestamp_param(timestamp, timer_mode) if timestamp.present?
false
end
if timestamp_seconds <= 0
return render_timer_validation_error('Timestamp must be in the future for countdown mode')
end
def validate_timestamp_seconds_param(timestamp_seconds)
seconds = Integer(timestamp_seconds)
return render_timer_validation_error('Timestamp must be in the future for countdown mode') if seconds <= 0
parsed_time = Time.zone.now + timestamp_seconds
params[:timestamp] = parsed_time
params[:timestamp] = Time.zone.now + seconds
params[:timer_mode] = 'countdown'
elsif timestamp.present?
begin
rescue ArgumentError
render_timer_validation_error('Invalid seconds format')
end
def validate_timestamp_param(timestamp, timer_mode)
parsed_time = Time.zone.parse(timestamp)
if parsed_time.nil?
return render_timer_validation_error('Invalid timestamp format')
@ -476,17 +492,7 @@ class TournamentsController < ApplicationController
params[:timestamp] = parsed_time
params[:timer_mode] = timer_mode.presence || 'countdown'
rescue ArgumentError
return render_timer_validation_error('Invalid timestamp format')
end
elsif timer_mode == 'countup'
params[:timestamp] = Time.zone.now
elsif timer_mode.present?
return render_timer_validation_error('timer_mode requires timestamp, timestamp_seconds, or clear_timer')
elsif timer_reason_update_requested
return render_timer_validation_error('timer_reason requires active timer') unless timer_active?
else
return render_timer_validation_error('timestamp, timestamp_seconds, or clear_timer is required')
end
render_timer_validation_error('Invalid timestamp format')
end
def normalize_timer_reason_params
@ -518,17 +524,13 @@ class TournamentsController < ApplicationController
timer_reason_param_present,
timer_reason_text_param_present
)
if timer_reason_text_param_present && !timer_reason_param_present
return 'timer_reason is required when timer_reason_text is provided'
end
return 'timer_reason is required when timer_reason_text is provided' if timer_reason_text_param_present && !timer_reason_param_present
return if timer_reason.blank? && timer_reason_text.blank?
return 'timer_reason_text is only allowed when timer_reason is custom' if timer_reason.blank?
return 'Invalid timer_reason' unless Tournament::TIMER_REASONS.include?(timer_reason)
if timer_reason == 'custom' && timer_reason_text.blank?
return 'timer_reason_text is required for custom timer_reason'
end
return 'timer_reason_text is required for custom timer_reason' if timer_reason == 'custom' && timer_reason_text.blank?
return unless timer_reason != 'custom' && timer_reason_text.present?

View File

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

View File

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

View File

@ -6,7 +6,8 @@ class TeamActionList < ApplicationRecord
validates :name, :key, :action_name, :source, presence: true
validates :key, uniqueness: { scope: :tournament_id }
validates :key, format: { with: /\A[a-z0-9_]+\z/, message: 'may only contain lowercase letters, numbers, and underscores' }
validates :key,
format: { with: /\A[a-z0-9_]+\z/, message: 'may only contain lowercase letters, numbers, and underscores' }
delegate :owner, to: :tournament
@ -20,7 +21,7 @@ class TeamActionList < ApplicationRecord
team_action_items.where.not(team_id: desired_team_ids).destroy_all
desired_team_ids.each do |team_id|
team_action_items.find_or_create_by!(team_id: team_id)
team_action_items.find_or_create_by!(team_id:)
end
end
end

View File

@ -9,7 +9,7 @@ class Tournament < ApplicationRecord
METADATA_STRING_ATTRIBUTES = %i[location website_url].freeze
belongs_to :user
belongs_to :owner, class_name: 'User', foreign_key: :user_id
belongs_to :owner, class_name: 'User', foreign_key: :user_id, inverse_of: :tournaments
has_many :teams, dependent: :destroy
has_many :stages, dependent: :destroy
has_many :beamers, dependent: :destroy

View File

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

View File

@ -3,18 +3,18 @@
class GroupStageService
class << self
def generate_group_stage(groups)
raise 'Cannot generate group stage without groups' if groups.length.zero?
raise 'Cannot generate group stage without groups' if groups.empty?
# raise an error if the average group size is not a whole number
raise 'Groups need to be equal size' unless (groups.flatten.length.to_f / groups.length % 1).zero?
groups = groups.map(&method(:get_group_object_from)).each_with_index { |group, i| group.number = i + 1 }
Stage.new level: -1, groups: groups, state: :in_progress
Stage.new level: -1, groups:, state: :in_progress
end
def get_group_object_from(team_array)
Group.new matches: generate_all_matches_between(team_array),
group_scores: team_array.map { |team| GroupScore.new team: team }
group_scores: team_array.map { |team| GroupScore.new team: }
end
def deal_with_spacing_of_teams(matches, team_size)
@ -71,7 +71,7 @@ class GroupStageService
def update_group_scores(group)
changed_group_scores = []
group.teams.each do |team|
group_score = group.group_scores.find_by(team: team)
group_score = group.group_scores.find_by(team:)
matches = group.matches.select { |match| match.teams.include? team }
# reset previous values
group_score.group_points = 0
@ -239,11 +239,9 @@ class GroupStageService
start_index -= 1 while start_index.positive? && sorted_group_scores[start_index - 1].position == position
end_index = index
while end_index < sorted_group_scores.size - 1 && sorted_group_scores[end_index + 1].position == position
end_index += 1
end
end_index += 1 while end_index < sorted_group_scores.size - 1 && sorted_group_scores[end_index + 1].position == position
{ start_index: start_index, end_index: end_index }
{ start_index:, end_index: }
end
def advancing_slots_for_group(group)
@ -272,7 +270,7 @@ class GroupStageService
advancing_slots = advancing_slots_for_group(group)
group_ready_for_decider_check = regular_group_matches_finished?(group)
cache = sorted_group_scores.each_with_index.each_with_object({}) do |(group_score, index), decisions|
cache = sorted_group_scores.each_with_index.with_object({}) do |(group_score, index), decisions|
next if sorted_group_scores.size <= 1
comparison_index = comparison_partner_index(sorted_group_scores, index)
@ -297,7 +295,8 @@ class GroupStageService
tied: comparison_reason[:tied],
needs_decider_match: group_ready_for_decider_check &&
comparison_reason[:tied] &&
unresolved_tie_affects_advancing_placements?(sorted_group_scores, group_score, advancing_slots)
unresolved_tie_affects_advancing_placements?(sorted_group_scores, group_score,
advancing_slots)
}
end

View File

@ -16,7 +16,7 @@ class PlayoffStageService
initial_stage.state = :intermediate_stage unless initial_stage.matches.find(&:single_team?).nil?
playoffs << initial_stage
# empty stages are the stages, the tournament is filled with to have the matches ready for later
empty_stages = generate_stages_with_empty_matches(stage_count - 1, teams.size > 3)
empty_stages = generate_stages_with_empty_matches(stage_count - 1, include_third_place_match: teams.size > 3)
playoffs.concat empty_stages
playoffs
end
@ -25,7 +25,7 @@ class PlayoffStageService
#
# @param stage_count [Integer] number of stages to generate
# @return [Array] the generated stages
def generate_stages_with_empty_matches(stage_count, include_third_place_match = false)
def generate_stages_with_empty_matches(stage_count, include_third_place_match: false)
empty_stages = []
stage_count.times do |i|
match_count = 2**i
@ -82,7 +82,9 @@ class PlayoffStageService
# of which the winners advance into the match below
# depending on the position of the match, the companion match is either on the left or right of it
companion_match = find_companion_match(current_match.position, current_stage)
return populate_final_stage_matches(current_match, companion_match, next_stage) if semifinal_match?(current_stage, next_stage)
return populate_final_stage_matches(current_match, companion_match, next_stage) if semifinal_match?(
current_stage, next_stage
)
winners = get_winners_of(companion_match, current_match)
populate_match!(match: next_stage.matches.find { |m| m.position == current_match.position / 2 }, teams: winners)

View File

@ -3,7 +3,7 @@
class TeamActionItemUpdate
class << self
def call(team_action_item:, completed:)
new(team_action_item: team_action_item, completed: completed).call
new(team_action_item:, completed:).call
end
end
@ -13,7 +13,7 @@ class TeamActionItemUpdate
end
def call
return false unless team_action_item.update(completed: completed)
return false unless team_action_item.update(completed:)
TournamentSyncEnqueue.call(team_action_item.tournament)
true

View File

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

View File

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

View File

@ -13,7 +13,7 @@ class TournamentSyncEnqueue
return false unless tournament.sync_push_enabled?
payload = TournamentSnapshotBuilder.build(fresh_tournament)
entry = TournamentSyncQueueEntry.find_or_initialize_by(tournament: tournament)
entry = TournamentSyncQueueEntry.find_or_initialize_by(tournament:)
entry.snapshot_json = JSON.dump(payload)
entry.status = 'pending'
entry.attempts = 0

View File

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

View File

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

View File

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

View File

@ -50,6 +50,5 @@ module TurniereBackend
config.action_mailer.default_options = {
from: 'noreply@turnie.re'
}
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.
require "bootsnap/setup" # Speed up boot time by caching expensive operations.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
require 'bundler/setup' # Set up gems listed in the Gemfile.
require 'bootsnap/setup' # Speed up boot time by caching expensive operations.

View File

@ -16,7 +16,7 @@ Rails.application.configure do
# Enable/disable caching. By default caching is disabled.
# Run rails dev:cache to toggle caching.
if Rails.root.join('tmp', 'caching-dev.txt').exist?
if Rails.root.join('tmp/caching-dev.txt').exist?
config.action_controller.perform_caching = true
config.cache_store = :memory_store

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,18 +1,20 @@
# frozen_string_literal: true
# This migration comes from active_storage (originally 20190112182829)
class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0]
def up
return unless table_exists?(:active_storage_blobs)
unless column_exists?(:active_storage_blobs, :service_name)
return if column_exists?(:active_storage_blobs, :service_name)
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)
end
change_column :active_storage_blobs, :service_name, :string, null: false
end
end
def down
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)
class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0]
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.string :variation_digest, null: false
t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true
t.index %i[blob_id variation_digest], name: 'index_active_storage_variant_records_uniqueness', unique: true
t.foreign_key :active_storage_blobs, column: :blob_id
end
end
private
def primary_key_type
config = Rails.configuration.generators
config.options[config.orm][:primary_key_type] || :primary_key

View File

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

View File

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

View File

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

View File

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

View File

@ -13,7 +13,7 @@ class CreateTeamActionLists < ActiveRecord::Migration[7.0]
t.timestamps
end
add_index :team_action_lists, [:tournament_id, :key], unique: true
add_index :team_action_lists, %i[tournament_id key], unique: true
create_table :team_action_items do |t|
t.references :team_action_list, null: false, foreign_key: { on_delete: :cascade }, index: true
@ -25,6 +25,6 @@ class CreateTeamActionLists < ActiveRecord::Migration[7.0]
t.timestamps
end
add_index :team_action_items, [:team_action_list_id, :team_id], unique: true
add_index :team_action_items, %i[team_action_list_id team_id], unique: true
end
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -73,9 +73,7 @@ result = runner.run
if options[:format] == 'json'
puts JSON.pretty_generate(result)
exit
end
else
puts "Load test base_url=#{result.fetch(:base_url)} tournament_id=#{result.dig(:tournament, :id)}"
result.fetch(:http).each do |level|
@ -102,3 +100,4 @@ puts(
"action_cable_connections_total=#{metrics.fetch(:action_cable_connections_total)}, " \
"ruby_threads=#{metrics.fetch(:ruby_threads)}"
)
end

View File

@ -28,7 +28,7 @@ module Local
result = yield
end
duration = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000.0
@measurements << { name: name, duration: duration.round(1), query_count: query_count }
@measurements << { name:, duration: duration.round(1), query_count: }
result
end

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

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

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"
SPEC_ROOT = Path("spec")
E2E_SPEC_ROOT = SPEC_ROOT / "e2e" / "http"
LINT_TARGETS = (
"Gemfile",
"Rakefile",
"config.ru",
"app",
"config",
"db/migrate",
"db/seeds.rb",
"e2e",
"lib",
"script",
)
ROOT = Path(__file__).resolve().parent
DB_DIR = ROOT / "db"
GEMFILE_LOCK_PATH = ROOT / "Gemfile.lock"
@ -802,7 +814,7 @@ def test_shard(c, node_index=1, node_total=8, dry_run=False):
@task
def lint(c):
"""Run RuboCop."""
c.run(_bundle_exec("rubocop"), pty=True)
c.run(_bundle_exec("rubocop", *LINT_TARGETS, "--cache", "false"), pty=True)
@task(name="bootstrap-dev-user")