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:
commit
b8518a1242
|
|
@ -7,7 +7,6 @@ db/*.sqlite3.*
|
|||
docker/**
|
||||
.gitignore
|
||||
.hound.yml
|
||||
.rubocop.yml
|
||||
docker-compose.yml
|
||||
README.md
|
||||
.gitlab-ci.yml
|
||||
|
|
|
|||
|
|
@ -13,6 +13,21 @@ include:
|
|||
- project: 'turniere/turniere-infra'
|
||||
file: '/ci/pipeline.yaml'
|
||||
|
||||
lint:
|
||||
stage: test
|
||||
tags:
|
||||
- docker
|
||||
needs:
|
||||
- job: build_image
|
||||
image: $CI_REGISTRY_IMAGE/test/commits:$CI_COMMIT_SHA
|
||||
rules:
|
||||
- if: $CI_PIPELINE_SOURCE != "push"
|
||||
when: never
|
||||
- when: always
|
||||
script:
|
||||
- cd /app
|
||||
- inv lint
|
||||
|
||||
rails spec:
|
||||
stage: test
|
||||
tags:
|
||||
|
|
|
|||
50
.rubocop.yml
50
.rubocop.yml
|
|
@ -1,9 +1,3 @@
|
|||
inherit_from: .rubocop_todo.yml
|
||||
|
||||
inherit_mode:
|
||||
merge:
|
||||
- Exclude
|
||||
|
||||
plugins:
|
||||
- rubocop-performance
|
||||
- rubocop-rails
|
||||
|
|
@ -15,6 +9,7 @@ AllCops:
|
|||
Exclude:
|
||||
- "bin/**/*"
|
||||
- "db/schema.rb"
|
||||
- "spec/**/*.rb"
|
||||
|
||||
# Enable Rails
|
||||
Rails:
|
||||
|
|
@ -22,14 +17,14 @@ Rails:
|
|||
|
||||
# Commonly used screens these days easily fit more than 80 characters.
|
||||
Layout/LineLength:
|
||||
Max: 120
|
||||
Max: 140
|
||||
|
||||
# Too short methods lead to extraction of single-use methods, which can make
|
||||
# the code easier to read (by naming things), but can also clutter the class
|
||||
Metrics/MethodLength:
|
||||
Exclude:
|
||||
- "db/migrate/*"
|
||||
Max: 50
|
||||
Max: 60
|
||||
|
||||
# The guiding principle of classes is SRP, SRP can't be accurately measured by LoC
|
||||
Metrics/ClassLength:
|
||||
|
|
@ -41,9 +36,10 @@ Metrics/ModuleLength:
|
|||
Metrics/AbcSize:
|
||||
Exclude:
|
||||
- "db/migrate/*"
|
||||
Max: 50
|
||||
Max: 75
|
||||
|
||||
Metrics/BlockLength:
|
||||
Max: 30
|
||||
AllowedMethods:
|
||||
- "namespace"
|
||||
- "create_table"
|
||||
|
|
@ -51,6 +47,42 @@ Metrics/BlockLength:
|
|||
- "config/**/*.rb"
|
||||
- "spec/**/*.rb"
|
||||
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 22
|
||||
|
||||
Metrics/ParameterLists:
|
||||
Max: 7
|
||||
|
||||
Metrics/PerceivedComplexity:
|
||||
Max: 24
|
||||
|
||||
Rails/ReversibleMigration:
|
||||
Exclude:
|
||||
- "db/migrate/**/*"
|
||||
|
||||
Rails/SkipsModelValidations:
|
||||
Exclude:
|
||||
- "app/models/tournament_sync_queue_entry.rb"
|
||||
- "app/services/tournament_sync_pusher.rb"
|
||||
- "db/migrate/**/*"
|
||||
|
||||
# These are standalone CLI entrypoints where stdout is command output,
|
||||
# not Rails application logging.
|
||||
Rails/Output:
|
||||
Exclude:
|
||||
- "e2e/load_test.rb"
|
||||
- "script/e2e_scenarios.rb"
|
||||
- "script/wait_for_http.rb"
|
||||
|
||||
Rails/Delegate:
|
||||
Exclude:
|
||||
- "app/controllers/tournaments_controller.rb"
|
||||
- "e2e/lib/action_cable_client.rb"
|
||||
|
||||
Style/MultilineBlockChain:
|
||||
Exclude:
|
||||
- "e2e/lib/api_client.rb"
|
||||
|
||||
# Disable documentation checks for now
|
||||
# should be done with yard instead
|
||||
Style/Documentation:
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
@ -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
|
||||
|
|
|
|||
4
Gemfile
4
Gemfile
|
|
@ -44,8 +44,8 @@ group :test, optional: true do
|
|||
gem 'factory_bot_rails'
|
||||
gem 'faker'
|
||||
gem 'rspec-rails'
|
||||
gem 'shoulda-matchers'
|
||||
gem 'ruby-prof'
|
||||
gem 'shoulda-matchers'
|
||||
end
|
||||
|
||||
group :development, optional: true do
|
||||
|
|
@ -53,13 +53,13 @@ group :development, optional: true do
|
|||
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
|
||||
gem 'railroady'
|
||||
# Use sqlite3 as the database for Active Record
|
||||
gem 'sqlite3'
|
||||
gem 'rubocop'
|
||||
gem 'rubocop-performance'
|
||||
gem 'rubocop-rails'
|
||||
gem 'solargraph'
|
||||
gem 'spring'
|
||||
gem 'spring-watcher-listen', '~> 2.1'
|
||||
gem 'sqlite3'
|
||||
end
|
||||
|
||||
group :production do
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ class TournamentsController < ApplicationController
|
|||
before_action :set_tournament_for_show, only: %i[show]
|
||||
before_action :set_tournament,
|
||||
only: %i[update destroy destroy_playoffs set_timer timer sync_state test_sync start_next_group_matches]
|
||||
before_action :authenticate_user!, only: %i[create update destroy destroy_playoffs set_timer test_sync start_next_group_matches]
|
||||
before_action :authenticate_user!,
|
||||
only: %i[create update destroy destroy_playoffs set_timer test_sync start_next_group_matches]
|
||||
before_action -> { require_owner! @tournament.owner },
|
||||
only: %i[update destroy destroy_playoffs set_timer test_sync start_next_group_matches]
|
||||
before_action :validate_create_params, only: %i[create]
|
||||
|
|
@ -74,6 +75,7 @@ class TournamentsController < ApplicationController
|
|||
end
|
||||
|
||||
raise ActionController::ParameterMissing, :teams if teams.nil?
|
||||
|
||||
if group_stage
|
||||
params.require(:playoff_teams_amount)
|
||||
groups = organize_teams_in_groups(teams)
|
||||
|
|
@ -339,7 +341,7 @@ class TournamentsController < ApplicationController
|
|||
return if ActiveModel::Type::Boolean.new.cast(params[:read_only_mode])
|
||||
|
||||
teams = params['teams']
|
||||
return if teams.is_a?(Array) && teams.reject { |t| t.is_a? ActionController::Parameters }.count.zero?
|
||||
return if teams.is_a?(Array) && teams.count { |team| !team.is_a?(ActionController::Parameters) }.zero?
|
||||
|
||||
render json: { error: 'Invalid teams array' }, status: :unprocessable_content
|
||||
end
|
||||
|
|
@ -374,12 +376,16 @@ class TournamentsController < ApplicationController
|
|||
error: 'playoff_teams_amount, instant_finalists_amount and intermediate_round_participants_amount don\'t match'
|
||||
}, status: :unprocessable_content
|
||||
end
|
||||
|
||||
def sync_state_params
|
||||
params.require(:snapshot).permit!.to_h
|
||||
end
|
||||
|
||||
def authorize_sync_request!
|
||||
return render json: { error: 'Tournament is not configured to accept sync pushes' }, status: :unprocessable_content unless @tournament.sync_accepts_push?
|
||||
unless @tournament.sync_accepts_push?
|
||||
return render json: { error: 'Tournament is not configured to accept sync pushes' },
|
||||
status: :unprocessable_content
|
||||
end
|
||||
|
||||
token = request.authorization.to_s.delete_prefix('Bearer ').presence || request.headers['X-Tournament-Sync-Token'].to_s
|
||||
return render json: { error: 'Missing sync token' }, status: :unauthorized if token.blank?
|
||||
|
|
@ -425,13 +431,9 @@ class TournamentsController < ApplicationController
|
|||
timer_reason_text = params[:timer_reason_text]
|
||||
clear_timer = ActiveModel::Type::Boolean.new.cast(params[:clear_timer])
|
||||
|
||||
if clear_timer
|
||||
return validate_clear_timer_params(timestamp, timestamp_seconds, timer_mode, timer_reason, timer_reason_text)
|
||||
end
|
||||
return validate_clear_timer_params(timestamp, timestamp_seconds, timer_mode, timer_reason, timer_reason_text) if clear_timer
|
||||
|
||||
if timer_mode.present? && !Tournament::TIMER_MODES.include?(timer_mode)
|
||||
return render_timer_validation_error('Invalid timer_mode')
|
||||
end
|
||||
return render_timer_validation_error('Invalid timer_mode') if timer_mode.present? && !Tournament::TIMER_MODES.include?(timer_mode)
|
||||
|
||||
timer_reason_error = validate_timer_reason_params(
|
||||
timer_reason,
|
||||
|
|
@ -441,7 +443,20 @@ class TournamentsController < ApplicationController
|
|||
)
|
||||
return render_timer_validation_error(timer_reason_error) if timer_reason_error
|
||||
|
||||
# throw error if both timestamp and timestamp_seconds are present
|
||||
return if validate_timer_timestamp_params(timestamp, timestamp_seconds, timer_mode)
|
||||
|
||||
if timer_mode == 'countup'
|
||||
params[:timestamp] = Time.zone.now
|
||||
elsif timer_mode.present?
|
||||
render_timer_validation_error('timer_mode requires timestamp, timestamp_seconds, or clear_timer')
|
||||
elsif timer_reason_update_requested
|
||||
render_timer_validation_error('timer_reason requires active timer') unless timer_active?
|
||||
else
|
||||
render_timer_validation_error('timestamp, timestamp_seconds, or clear_timer is required')
|
||||
end
|
||||
end
|
||||
|
||||
def validate_timer_timestamp_params(timestamp, timestamp_seconds, timer_mode)
|
||||
if timestamp.present? && timestamp_seconds.present?
|
||||
return render_timer_validation_error('Only one of timestamp or timestamp_seconds is allowed')
|
||||
end
|
||||
|
|
@ -450,43 +465,34 @@ class TournamentsController < ApplicationController
|
|||
return render_timer_validation_error('timestamp_seconds is only allowed for countdown mode')
|
||||
end
|
||||
|
||||
if timestamp_seconds.present?
|
||||
begin
|
||||
timestamp_seconds = Integer(timestamp_seconds)
|
||||
rescue ArgumentError
|
||||
return render_timer_validation_error('Invalid seconds format')
|
||||
end
|
||||
return validate_timestamp_seconds_param(timestamp_seconds) if timestamp_seconds.present?
|
||||
return validate_timestamp_param(timestamp, timer_mode) if timestamp.present?
|
||||
|
||||
if timestamp_seconds <= 0
|
||||
return render_timer_validation_error('Timestamp must be in the future for countdown mode')
|
||||
end
|
||||
false
|
||||
end
|
||||
|
||||
parsed_time = Time.zone.now + timestamp_seconds
|
||||
params[:timestamp] = parsed_time
|
||||
params[:timer_mode] = 'countdown'
|
||||
elsif timestamp.present?
|
||||
begin
|
||||
parsed_time = Time.zone.parse(timestamp)
|
||||
if parsed_time.nil?
|
||||
return render_timer_validation_error('Invalid timestamp format')
|
||||
elsif timer_mode != 'countup' && !parsed_time.future?
|
||||
return render_timer_validation_error('Timestamp must be in the future for countdown mode')
|
||||
end
|
||||
def validate_timestamp_seconds_param(timestamp_seconds)
|
||||
seconds = Integer(timestamp_seconds)
|
||||
return render_timer_validation_error('Timestamp must be in the future for countdown mode') if seconds <= 0
|
||||
|
||||
params[:timestamp] = parsed_time
|
||||
params[:timer_mode] = timer_mode.presence || 'countdown'
|
||||
rescue ArgumentError
|
||||
return render_timer_validation_error('Invalid timestamp format')
|
||||
end
|
||||
elsif timer_mode == 'countup'
|
||||
params[:timestamp] = Time.zone.now
|
||||
elsif timer_mode.present?
|
||||
return render_timer_validation_error('timer_mode requires timestamp, timestamp_seconds, or clear_timer')
|
||||
elsif timer_reason_update_requested
|
||||
return render_timer_validation_error('timer_reason requires active timer') unless timer_active?
|
||||
else
|
||||
return render_timer_validation_error('timestamp, timestamp_seconds, or clear_timer is required')
|
||||
params[:timestamp] = Time.zone.now + seconds
|
||||
params[:timer_mode] = 'countdown'
|
||||
rescue ArgumentError
|
||||
render_timer_validation_error('Invalid seconds format')
|
||||
end
|
||||
|
||||
def validate_timestamp_param(timestamp, timer_mode)
|
||||
parsed_time = Time.zone.parse(timestamp)
|
||||
if parsed_time.nil?
|
||||
return render_timer_validation_error('Invalid timestamp format')
|
||||
elsif timer_mode != 'countup' && !parsed_time.future?
|
||||
return render_timer_validation_error('Timestamp must be in the future for countdown mode')
|
||||
end
|
||||
|
||||
params[:timestamp] = parsed_time
|
||||
params[:timer_mode] = timer_mode.presence || 'countdown'
|
||||
rescue ArgumentError
|
||||
render_timer_validation_error('Invalid timestamp format')
|
||||
end
|
||||
|
||||
def normalize_timer_reason_params
|
||||
|
|
@ -518,17 +524,13 @@ class TournamentsController < ApplicationController
|
|||
timer_reason_param_present,
|
||||
timer_reason_text_param_present
|
||||
)
|
||||
if timer_reason_text_param_present && !timer_reason_param_present
|
||||
return 'timer_reason is required when timer_reason_text is provided'
|
||||
end
|
||||
return 'timer_reason is required when timer_reason_text is provided' if timer_reason_text_param_present && !timer_reason_param_present
|
||||
return if timer_reason.blank? && timer_reason_text.blank?
|
||||
|
||||
return 'timer_reason_text is only allowed when timer_reason is custom' if timer_reason.blank?
|
||||
return 'Invalid timer_reason' unless Tournament::TIMER_REASONS.include?(timer_reason)
|
||||
|
||||
if timer_reason == 'custom' && timer_reason_text.blank?
|
||||
return 'timer_reason_text is required for custom timer_reason'
|
||||
end
|
||||
return 'timer_reason_text is required for custom timer_reason' if timer_reason == 'custom' && timer_reason_text.blank?
|
||||
|
||||
return unless timer_reason != 'custom' && timer_reason_text.present?
|
||||
|
||||
|
|
|
|||
|
|
@ -16,9 +16,9 @@ class AdvanceTeamsInIntermediateStage
|
|||
|
||||
# populate stage below with the "winners" from single team matches
|
||||
matches.select { |m| m.state == 'single_team' }
|
||||
.each do |match|
|
||||
context.fail! unless PopulateMatchBelowAndSave.call(match: match).success?
|
||||
end
|
||||
.each do |match|
|
||||
context.fail! unless PopulateMatchBelowAndSave.call(match:).success?
|
||||
end
|
||||
(context.object_to_save ||= []) << intermediate_stage
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ class TournamentSerializer < SimpleTournamentSerializer
|
|||
attribute :sync_target_url, if: :sync_metadata_visible?
|
||||
attribute :sync_auth_configured, if: :sync_metadata_visible?
|
||||
attribute :sync_last_push_error, if: :sync_metadata_visible?
|
||||
# NEVER expose sync_auth_token anywhere - it should only ever be written to or checked against
|
||||
# NEVER expose sync_auth_token anywhere - it should only ever be written to or checked against
|
||||
|
||||
has_many :stages
|
||||
has_many :team_action_lists
|
||||
|
|
|
|||
|
|
@ -3,18 +3,18 @@
|
|||
class GroupStageService
|
||||
class << self
|
||||
def generate_group_stage(groups)
|
||||
raise 'Cannot generate group stage without groups' if groups.length.zero?
|
||||
raise 'Cannot generate group stage without groups' if groups.empty?
|
||||
|
||||
# raise an error if the average group size is not a whole number
|
||||
raise 'Groups need to be equal size' unless (groups.flatten.length.to_f / groups.length % 1).zero?
|
||||
|
||||
groups = groups.map(&method(:get_group_object_from)).each_with_index { |group, i| group.number = i + 1 }
|
||||
Stage.new level: -1, groups: groups, state: :in_progress
|
||||
Stage.new level: -1, groups:, state: :in_progress
|
||||
end
|
||||
|
||||
def get_group_object_from(team_array)
|
||||
Group.new matches: generate_all_matches_between(team_array),
|
||||
group_scores: team_array.map { |team| GroupScore.new team: team }
|
||||
group_scores: team_array.map { |team| GroupScore.new team: }
|
||||
end
|
||||
|
||||
def deal_with_spacing_of_teams(matches, team_size)
|
||||
|
|
@ -71,7 +71,7 @@ class GroupStageService
|
|||
def update_group_scores(group)
|
||||
changed_group_scores = []
|
||||
group.teams.each do |team|
|
||||
group_score = group.group_scores.find_by(team: team)
|
||||
group_score = group.group_scores.find_by(team:)
|
||||
matches = group.matches.select { |match| match.teams.include? team }
|
||||
# reset previous values
|
||||
group_score.group_points = 0
|
||||
|
|
@ -217,7 +217,7 @@ class GroupStageService
|
|||
def comparison_partner_index(sorted_group_scores, index)
|
||||
return index - 1 if index.positive? && sorted_group_scores[index - 1].position == sorted_group_scores[index].position
|
||||
return index + 1 if index < sorted_group_scores.size - 1 &&
|
||||
sorted_group_scores[index + 1].position == sorted_group_scores[index].position
|
||||
sorted_group_scores[index + 1].position == sorted_group_scores[index].position
|
||||
return index - 1 if index.positive?
|
||||
return index + 1 if sorted_group_scores.size > 1
|
||||
|
||||
|
|
@ -239,11 +239,9 @@ class GroupStageService
|
|||
start_index -= 1 while start_index.positive? && sorted_group_scores[start_index - 1].position == position
|
||||
|
||||
end_index = index
|
||||
while end_index < sorted_group_scores.size - 1 && sorted_group_scores[end_index + 1].position == position
|
||||
end_index += 1
|
||||
end
|
||||
end_index += 1 while end_index < sorted_group_scores.size - 1 && sorted_group_scores[end_index + 1].position == position
|
||||
|
||||
{ start_index: start_index, end_index: end_index }
|
||||
{ start_index:, end_index: }
|
||||
end
|
||||
|
||||
def advancing_slots_for_group(group)
|
||||
|
|
@ -272,7 +270,7 @@ class GroupStageService
|
|||
advancing_slots = advancing_slots_for_group(group)
|
||||
group_ready_for_decider_check = regular_group_matches_finished?(group)
|
||||
|
||||
cache = sorted_group_scores.each_with_index.each_with_object({}) do |(group_score, index), decisions|
|
||||
cache = sorted_group_scores.each_with_index.with_object({}) do |(group_score, index), decisions|
|
||||
next if sorted_group_scores.size <= 1
|
||||
|
||||
comparison_index = comparison_partner_index(sorted_group_scores, index)
|
||||
|
|
@ -296,8 +294,9 @@ class GroupStageService
|
|||
decider_match_id: comparison_reason[:decider_match_id],
|
||||
tied: comparison_reason[:tied],
|
||||
needs_decider_match: group_ready_for_decider_check &&
|
||||
comparison_reason[:tied] &&
|
||||
unresolved_tie_affects_advancing_placements?(sorted_group_scores, group_score, advancing_slots)
|
||||
comparison_reason[:tied] &&
|
||||
unresolved_tie_affects_advancing_placements?(sorted_group_scores, group_score,
|
||||
advancing_slots)
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -50,6 +50,5 @@ module TurniereBackend
|
|||
config.action_mailer.default_options = {
|
||||
from: 'noreply@turnie.re'
|
||||
}
|
||||
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# This migration comes from active_storage (originally 20190112182829)
|
||||
class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0]
|
||||
def up
|
||||
return unless table_exists?(:active_storage_blobs)
|
||||
|
||||
unless column_exists?(:active_storage_blobs, :service_name)
|
||||
add_column :active_storage_blobs, :service_name, :string
|
||||
return if column_exists?(:active_storage_blobs, :service_name)
|
||||
|
||||
if configured_service = ActiveStorage::Blob.service.name
|
||||
ActiveStorage::Blob.unscoped.update_all(service_name: configured_service)
|
||||
end
|
||||
add_column :active_storage_blobs, :service_name, :string
|
||||
|
||||
change_column :active_storage_blobs, :service_name, :string, null: false
|
||||
if (configured_service = ActiveStorage::Blob.service.name)
|
||||
ActiveStorage::Blob.unscoped.update_all(service_name: configured_service)
|
||||
end
|
||||
|
||||
change_column :active_storage_blobs, :service_name, :string, null: false
|
||||
end
|
||||
|
||||
def down
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# This migration comes from active_storage (originally 20191206030411)
|
||||
class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0]
|
||||
def change
|
||||
|
|
@ -8,20 +10,21 @@ class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0]
|
|||
t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type
|
||||
t.string :variation_digest, null: false
|
||||
|
||||
t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true
|
||||
t.index %i[blob_id variation_digest], name: 'index_active_storage_variant_records_uniqueness', unique: true
|
||||
t.foreign_key :active_storage_blobs, column: :blob_id
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
def primary_key_type
|
||||
config = Rails.configuration.generators
|
||||
config.options[config.orm][:primary_key_type] || :primary_key
|
||||
end
|
||||
|
||||
def blobs_primary_key_type
|
||||
pkey_name = connection.primary_key(:active_storage_blobs)
|
||||
pkey_column = connection.columns(:active_storage_blobs).find { |c| c.name == pkey_name }
|
||||
pkey_column.bigint? ? :bigint : pkey_column.type
|
||||
end
|
||||
def primary_key_type
|
||||
config = Rails.configuration.generators
|
||||
config.options[config.orm][:primary_key_type] || :primary_key
|
||||
end
|
||||
|
||||
def blobs_primary_key_type
|
||||
pkey_name = connection.primary_key(:active_storage_blobs)
|
||||
pkey_column = connection.columns(:active_storage_blobs).find { |c| c.name == pkey_name }
|
||||
pkey_column.bigint? ? :bigint : pkey_column.type
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# This migration comes from active_storage (originally 20211119233751)
|
||||
class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0]
|
||||
def change
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddPositionToGroupScores < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_column :group_scores, :position, :integer
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddDefaultToPositionInGroupScores < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
change_column_default :group_scores, :position, 0
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddTimerEndToTournaments < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_column :tournaments, :timer_end, :datetime
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddTimerModeToTournaments < ActiveRecord::Migration[7.0]
|
||||
def change
|
||||
add_column :tournaments, :timer_mode, :string
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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?
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ require 'uri'
|
|||
|
||||
module TurniereE2E
|
||||
class ScenarioRunner
|
||||
DEFAULT_PASSWORD = 'password123'.freeze
|
||||
DEFAULT_PASSWORD = 'password123'
|
||||
|
||||
attr_reader :client
|
||||
|
||||
|
|
@ -36,19 +36,19 @@ module TurniereE2E
|
|||
timestamp: timer_response.fetch(:timestamp),
|
||||
timer_reason: timer_response.fetch(:timer_reason)
|
||||
}
|
||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :timer_set
|
||||
return checkpoint_result(session, tournament, checkpoints, stop_at) if stop_at == :timer_set
|
||||
|
||||
upcoming_matches = get!(
|
||||
"/tournaments/#{tournament.fetch(:id)}/matches",
|
||||
params: { state: 'upcoming' }
|
||||
)
|
||||
checkpoints[:upcoming_group_matches] = { match_ids: upcoming_matches.map { |match| match.fetch(:id) } }
|
||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :upcoming_group_matches
|
||||
return checkpoint_result(session, tournament, checkpoints, stop_at) if stop_at == :upcoming_group_matches
|
||||
|
||||
finish_group_stage_matches!(tournament)
|
||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||
checkpoints[:groups_finished] = summarize_tournament(tournament)
|
||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :groups_finished
|
||||
return checkpoint_result(session, tournament, checkpoints, stop_at) if stop_at == :groups_finished
|
||||
|
||||
statistics = get!("/tournaments/#{tournament.fetch(:id)}/statistics")
|
||||
checkpoints[:statistics] = {
|
||||
|
|
@ -60,12 +60,12 @@ module TurniereE2E
|
|||
finish_stage!(group_stage_of!(tournament).fetch(:id))
|
||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||
checkpoints[:playoffs_created] = summarize_tournament(tournament)
|
||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :playoffs_created
|
||||
return checkpoint_result(session, tournament, checkpoints, stop_at) if stop_at == :playoffs_created
|
||||
|
||||
run_playoffs_until_before_final!(tournament.fetch(:id))
|
||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||
checkpoints[:before_final_finish_attempt] = summarize_tournament(tournament)
|
||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :before_final_finish_attempt
|
||||
return checkpoint_result(session, tournament, checkpoints, stop_at) if stop_at == :before_final_finish_attempt
|
||||
|
||||
finish_attempt = finish_final_stage_matches!(tournament)
|
||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||
|
|
@ -86,18 +86,27 @@ module TurniereE2E
|
|||
checkpoints = {
|
||||
created: summarize_tournament(tournament)
|
||||
}
|
||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :created
|
||||
if stop_at == :created
|
||||
return result(session: session, tournament: tournament, checkpoints: checkpoints,
|
||||
stop_at: stop_at)
|
||||
end
|
||||
|
||||
finish_group_stage_matches!(tournament)
|
||||
finish_stage!(group_stage_of!(fetch_tournament!(tournament.fetch(:id))).fetch(:id))
|
||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||
checkpoints[:playoffs_created] = summarize_tournament(tournament)
|
||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :playoffs_created
|
||||
if stop_at == :playoffs_created
|
||||
return result(session: session, tournament: tournament, checkpoints: checkpoints,
|
||||
stop_at: stop_at)
|
||||
end
|
||||
|
||||
run_playoffs_until_before_final!(tournament.fetch(:id))
|
||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||
checkpoints[:before_final_finish_attempt] = summarize_tournament(tournament)
|
||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :before_final_finish_attempt
|
||||
if stop_at == :before_final_finish_attempt
|
||||
return result(session: session, tournament: tournament, checkpoints: checkpoints,
|
||||
stop_at: stop_at)
|
||||
end
|
||||
|
||||
finish_attempt = finish_final_stage_matches!(tournament)
|
||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||
|
|
@ -132,7 +141,10 @@ module TurniereE2E
|
|||
checkpoints[created_key] = summarize_tournament(tournament).merge(
|
||||
playoff_matchups: playoff_matchups(tournament)
|
||||
)
|
||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == created_key
|
||||
if stop_at == created_key
|
||||
return result(session: session, tournament: tournament, checkpoints: checkpoints,
|
||||
stop_at: stop_at)
|
||||
end
|
||||
|
||||
next if index == rounds.size - 1
|
||||
|
||||
|
|
@ -140,7 +152,10 @@ module TurniereE2E
|
|||
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||
deleted_key = deleted_keys.fetch(index)
|
||||
checkpoints[deleted_key] = summarize_tournament(tournament)
|
||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == deleted_key
|
||||
if stop_at == deleted_key
|
||||
return result(session: session, tournament: tournament, checkpoints: checkpoints,
|
||||
stop_at: stop_at)
|
||||
end
|
||||
end
|
||||
|
||||
result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at)
|
||||
|
|
@ -152,7 +167,10 @@ module TurniereE2E
|
|||
checkpoints = {
|
||||
created: summarize_tournament(tournament)
|
||||
}
|
||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :created
|
||||
if stop_at == :created
|
||||
return result(session: session, tournament: tournament, checkpoints: checkpoints,
|
||||
stop_at: stop_at)
|
||||
end
|
||||
|
||||
run_playoffs_until_complete!(tournament.fetch(:id))
|
||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||
|
|
@ -170,7 +188,10 @@ module TurniereE2E
|
|||
checkpoints = {
|
||||
created: summarize_tournament(tournament)
|
||||
}
|
||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :created
|
||||
if stop_at == :created
|
||||
return result(session: session, tournament: tournament, checkpoints: checkpoints,
|
||||
stop_at: stop_at)
|
||||
end
|
||||
|
||||
group = group_stage_of!(tournament).fetch(:groups).first
|
||||
play_group_matches_with_explicit_scores!(group, group_direct_comparison_score_plan(case_name))
|
||||
|
|
@ -250,7 +271,10 @@ module TurniereE2E
|
|||
checkpoints = {
|
||||
created: summarize_tournament(tournament)
|
||||
}
|
||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :created
|
||||
if stop_at == :created
|
||||
return result(session: session, tournament: tournament, checkpoints: checkpoints,
|
||||
stop_at: stop_at)
|
||||
end
|
||||
|
||||
profiled_show = fetch_tournament_profile!(tournament.fetch(:id))
|
||||
checkpoints[:profiled_show] = profiled_show
|
||||
|
|
@ -259,7 +283,7 @@ module TurniereE2E
|
|||
end
|
||||
|
||||
def wait_for_healthcheck!(timeout_seconds: 30)
|
||||
started_at = Time.now
|
||||
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
loop do
|
||||
begin
|
||||
response = client.get('/healthz')
|
||||
|
|
@ -268,7 +292,8 @@ module TurniereE2E
|
|||
nil
|
||||
end
|
||||
|
||||
raise "healthcheck did not become ready within #{timeout_seconds}s" if Time.now - started_at > timeout_seconds
|
||||
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
|
||||
raise "healthcheck did not become ready within #{timeout_seconds}s" if elapsed > timeout_seconds
|
||||
|
||||
sleep 0.5
|
||||
end
|
||||
|
|
@ -282,7 +307,7 @@ module TurniereE2E
|
|||
unique = SecureRandom.hex(6)
|
||||
email = "e2e-#{unique}@example.com"
|
||||
username = "e2e-#{unique}"
|
||||
response = client.register!(email: email, password: DEFAULT_PASSWORD, username: username)
|
||||
client.register!(email: email, password: DEFAULT_PASSWORD, username: username)
|
||||
return { email: email, username: username } if client.authenticated?
|
||||
|
||||
login_response = client.login!(email: email, password: DEFAULT_PASSWORD)
|
||||
|
|
@ -350,9 +375,12 @@ module TurniereE2E
|
|||
def run_playoffs_until_before_final!(tournament_id)
|
||||
loop do
|
||||
tournament = fetch_tournament!(tournament_id)
|
||||
playable_matches = playoff_matches(tournament).reject { |match| match.fetch(:stage_level) == 0 }
|
||||
.select { |match| match.fetch(:state) == 'not_started' }
|
||||
.sort_by { |match| [-match.fetch(:stage_level), match.fetch(:position)] }
|
||||
playable_matches = playoff_matches(tournament).reject { |match| match.fetch(:stage_level).zero? }
|
||||
.select { |match| match.fetch(:state) == 'not_started' }
|
||||
.sort_by do |match|
|
||||
[-match.fetch(:stage_level),
|
||||
match.fetch(:position)]
|
||||
end
|
||||
|
||||
break if playable_matches.empty?
|
||||
|
||||
|
|
@ -366,7 +394,10 @@ module TurniereE2E
|
|||
loop do
|
||||
tournament = fetch_tournament!(tournament_id)
|
||||
playable_matches = playoff_matches(tournament).select { |match| match.fetch(:state) == 'not_started' }
|
||||
.sort_by { |match| [-match.fetch(:stage_level), match.fetch(:position)] }
|
||||
.sort_by do |match|
|
||||
[-match.fetch(:stage_level),
|
||||
match.fetch(:position)]
|
||||
end
|
||||
break if playable_matches.empty?
|
||||
|
||||
playable_matches.each do |match|
|
||||
|
|
@ -390,7 +421,9 @@ module TurniereE2E
|
|||
expect_status!(finished, expected_finish_status, "finish match #{match.fetch(:id)}")
|
||||
if expected_finish_error
|
||||
actual_error = finished.fetch(:json).fetch(:error)
|
||||
raise "expected finish error #{expected_finish_error.inspect}, got #{actual_error.inspect}" unless actual_error == expected_finish_error
|
||||
unless actual_error == expected_finish_error
|
||||
raise "expected finish error #{expected_finish_error.inspect}, got #{actual_error.inspect}"
|
||||
end
|
||||
end
|
||||
|
||||
{
|
||||
|
|
@ -418,7 +451,9 @@ module TurniereE2E
|
|||
expect_status!(finished, expected_finish_status, "finish match #{match.fetch(:id)}")
|
||||
if expected_finish_error
|
||||
actual_error = finished.fetch(:json).fetch(:error)
|
||||
raise "expected finish error #{expected_finish_error.inspect}, got #{actual_error.inspect}" unless actual_error == expected_finish_error
|
||||
unless actual_error == expected_finish_error
|
||||
raise "expected finish error #{expected_finish_error.inspect}, got #{actual_error.inspect}"
|
||||
end
|
||||
end
|
||||
|
||||
{
|
||||
|
|
@ -481,7 +516,9 @@ module TurniereE2E
|
|||
end
|
||||
|
||||
def final_stage_matches_of!(tournament)
|
||||
final_stage = tournament.fetch(:stages).find { |stage| stage.fetch(:level) == 0 } || raise('final stage not found')
|
||||
final_stage = tournament.fetch(:stages).find do |stage|
|
||||
stage.fetch(:level).zero?
|
||||
end || raise('final stage not found')
|
||||
attach_stage_level(final_stage).fetch(:matches).sort_by { |match| match.fetch(:position) }
|
||||
end
|
||||
|
||||
|
|
@ -496,7 +533,9 @@ module TurniereE2E
|
|||
|
||||
{
|
||||
status: 200,
|
||||
finished_match_ids: final_stage_matches_of!(fetch_tournament!(tournament.fetch(:id))).map { |match| match.fetch(:id) }
|
||||
finished_match_ids: final_stage_matches_of!(fetch_tournament!(tournament.fetch(:id))).map do |match|
|
||||
match.fetch(:id)
|
||||
end
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -574,7 +613,7 @@ module TurniereE2E
|
|||
|
||||
def create_group_stage_payload(group_count:, teams_per_group:, playoff_teams_amount:)
|
||||
teams = group_count.times.flat_map do |group_index|
|
||||
teams_per_group.times.map do |seed_index|
|
||||
Array.new(teams_per_group) do |seed_index|
|
||||
{
|
||||
name: "G#{group_index + 1}-S#{seed_index + 1}",
|
||||
group: group_index
|
||||
|
|
@ -626,7 +665,7 @@ module TurniereE2E
|
|||
name: "HTTP E2E Playoff Only #{team_count} #{SecureRandom.hex(3)}",
|
||||
description: 'HTTP lifecycle E2E tournament',
|
||||
public: false,
|
||||
teams: team_count.times.map { |index| { name: "G0-S#{index + 1}" } }
|
||||
teams: Array.new(team_count) { |index| { name: "G0-S#{index + 1}" } }
|
||||
}
|
||||
end
|
||||
|
||||
|
|
@ -739,7 +778,7 @@ module TurniereE2E
|
|||
end
|
||||
|
||||
def parse_server_timing(header)
|
||||
return {} if header.nil? || header.empty?
|
||||
return {} if header.to_s.empty?
|
||||
|
||||
header.split(',').each_with_object({}) do |entry, sections|
|
||||
name, duration = entry.strip.match(/\A([^;]+);dur=([0-9.]+)\z/)&.captures
|
||||
|
|
@ -758,6 +797,10 @@ module TurniereE2E
|
|||
}
|
||||
end
|
||||
|
||||
def checkpoint_result(session, tournament, checkpoints, stop_at)
|
||||
result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at)
|
||||
end
|
||||
|
||||
def expect_status!(response, expected_status, action)
|
||||
return if response.fetch(:status) == expected_status
|
||||
|
||||
|
|
|
|||
|
|
@ -73,32 +73,31 @@ result = runner.run
|
|||
|
||||
if options[:format] == 'json'
|
||||
puts JSON.pretty_generate(result)
|
||||
exit
|
||||
end
|
||||
else
|
||||
puts "Load test base_url=#{result.fetch(:base_url)} tournament_id=#{result.dig(:tournament, :id)}"
|
||||
|
||||
puts "Load test base_url=#{result.fetch(:base_url)} tournament_id=#{result.dig(:tournament, :id)}"
|
||||
result.fetch(:http).each do |level|
|
||||
puts(
|
||||
"HTTP #{level.fetch(:clients)} clients: " \
|
||||
"#{level.fetch(:ok)}/#{level.fetch(:requests)} ok, errors=#{level.fetch(:errors)}, " \
|
||||
"p50=#{level.fetch(:p50_ms)}ms, p95=#{level.fetch(:p95_ms)}ms, " \
|
||||
"max=#{level.fetch(:max_ms)}ms, rps=#{level.fetch(:requests_per_second)}"
|
||||
)
|
||||
end
|
||||
|
||||
result.fetch(:http).each do |level|
|
||||
result.fetch(:websocket).each do |level|
|
||||
puts(
|
||||
"WS #{level.fetch(:clients)} clients: " \
|
||||
"opened=#{level.fetch(:opened)}, errors=#{level.fetch(:errors)}, " \
|
||||
"p50_open=#{level.fetch(:p50_open_ms)}ms, p95_open=#{level.fetch(:p95_open_ms)}ms, " \
|
||||
"connections_metric=#{level.fetch(:connection_metric)}"
|
||||
)
|
||||
end
|
||||
|
||||
metrics = result.fetch(:metrics)
|
||||
puts(
|
||||
"HTTP #{level.fetch(:clients)} clients: " \
|
||||
"#{level.fetch(:ok)}/#{level.fetch(:requests)} ok, errors=#{level.fetch(:errors)}, " \
|
||||
"p50=#{level.fetch(:p50_ms)}ms, p95=#{level.fetch(:p95_ms)}ms, " \
|
||||
"max=#{level.fetch(:max_ms)}ms, rps=#{level.fetch(:requests_per_second)}"
|
||||
"Metrics: action_cable_connections=#{metrics.fetch(:action_cable_connections)}, " \
|
||||
"action_cable_connections_total=#{metrics.fetch(:action_cable_connections_total)}, " \
|
||||
"ruby_threads=#{metrics.fetch(:ruby_threads)}"
|
||||
)
|
||||
end
|
||||
|
||||
result.fetch(:websocket).each do |level|
|
||||
puts(
|
||||
"WS #{level.fetch(:clients)} clients: " \
|
||||
"opened=#{level.fetch(:opened)}, errors=#{level.fetch(:errors)}, " \
|
||||
"p50_open=#{level.fetch(:p50_open_ms)}ms, p95_open=#{level.fetch(:p95_open_ms)}ms, " \
|
||||
"connections_metric=#{level.fetch(:connection_metric)}"
|
||||
)
|
||||
end
|
||||
|
||||
metrics = result.fetch(:metrics)
|
||||
puts(
|
||||
"Metrics: action_cable_connections=#{metrics.fetch(:action_cable_connections)}, " \
|
||||
"action_cable_connections_total=#{metrics.fetch(:action_cable_connections_total)}, " \
|
||||
"ruby_threads=#{metrics.fetch(:ruby_threads)}"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
14
tasks.py
14
tasks.py
|
|
@ -57,6 +57,18 @@ BLACKBOX_MAILGUN_API_KEY = "blackbox-test-api-key"
|
|||
BLACKBOX_MAILGUN_DOMAIN = "blackbox.example.com"
|
||||
SPEC_ROOT = Path("spec")
|
||||
E2E_SPEC_ROOT = SPEC_ROOT / "e2e" / "http"
|
||||
LINT_TARGETS = (
|
||||
"Gemfile",
|
||||
"Rakefile",
|
||||
"config.ru",
|
||||
"app",
|
||||
"config",
|
||||
"db/migrate",
|
||||
"db/seeds.rb",
|
||||
"e2e",
|
||||
"lib",
|
||||
"script",
|
||||
)
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
DB_DIR = ROOT / "db"
|
||||
GEMFILE_LOCK_PATH = ROOT / "Gemfile.lock"
|
||||
|
|
@ -802,7 +814,7 @@ def test_shard(c, node_index=1, node_total=8, dry_run=False):
|
|||
@task
|
||||
def lint(c):
|
||||
"""Run RuboCop."""
|
||||
c.run(_bundle_exec("rubocop"), pty=True)
|
||||
c.run(_bundle_exec("rubocop", *LINT_TARGETS, "--cache", "false"), pty=True)
|
||||
|
||||
|
||||
@task(name="bootstrap-dev-user")
|
||||
|
|
|
|||
Loading…
Reference in New Issue