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/**
|
docker/**
|
||||||
.gitignore
|
.gitignore
|
||||||
.hound.yml
|
.hound.yml
|
||||||
.rubocop.yml
|
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
README.md
|
README.md
|
||||||
.gitlab-ci.yml
|
.gitlab-ci.yml
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,21 @@ include:
|
||||||
- project: 'turniere/turniere-infra'
|
- project: 'turniere/turniere-infra'
|
||||||
file: '/ci/pipeline.yaml'
|
file: '/ci/pipeline.yaml'
|
||||||
|
|
||||||
|
lint:
|
||||||
|
stage: test
|
||||||
|
tags:
|
||||||
|
- docker
|
||||||
|
needs:
|
||||||
|
- job: build_image
|
||||||
|
image: $CI_REGISTRY_IMAGE/test/commits:$CI_COMMIT_SHA
|
||||||
|
rules:
|
||||||
|
- if: $CI_PIPELINE_SOURCE != "push"
|
||||||
|
when: never
|
||||||
|
- when: always
|
||||||
|
script:
|
||||||
|
- cd /app
|
||||||
|
- inv lint
|
||||||
|
|
||||||
rails spec:
|
rails spec:
|
||||||
stage: test
|
stage: test
|
||||||
tags:
|
tags:
|
||||||
|
|
|
||||||
50
.rubocop.yml
50
.rubocop.yml
|
|
@ -1,9 +1,3 @@
|
||||||
inherit_from: .rubocop_todo.yml
|
|
||||||
|
|
||||||
inherit_mode:
|
|
||||||
merge:
|
|
||||||
- Exclude
|
|
||||||
|
|
||||||
plugins:
|
plugins:
|
||||||
- rubocop-performance
|
- rubocop-performance
|
||||||
- rubocop-rails
|
- rubocop-rails
|
||||||
|
|
@ -15,6 +9,7 @@ AllCops:
|
||||||
Exclude:
|
Exclude:
|
||||||
- "bin/**/*"
|
- "bin/**/*"
|
||||||
- "db/schema.rb"
|
- "db/schema.rb"
|
||||||
|
- "spec/**/*.rb"
|
||||||
|
|
||||||
# Enable Rails
|
# Enable Rails
|
||||||
Rails:
|
Rails:
|
||||||
|
|
@ -22,14 +17,14 @@ Rails:
|
||||||
|
|
||||||
# Commonly used screens these days easily fit more than 80 characters.
|
# Commonly used screens these days easily fit more than 80 characters.
|
||||||
Layout/LineLength:
|
Layout/LineLength:
|
||||||
Max: 120
|
Max: 140
|
||||||
|
|
||||||
# Too short methods lead to extraction of single-use methods, which can make
|
# Too short methods lead to extraction of single-use methods, which can make
|
||||||
# the code easier to read (by naming things), but can also clutter the class
|
# the code easier to read (by naming things), but can also clutter the class
|
||||||
Metrics/MethodLength:
|
Metrics/MethodLength:
|
||||||
Exclude:
|
Exclude:
|
||||||
- "db/migrate/*"
|
- "db/migrate/*"
|
||||||
Max: 50
|
Max: 60
|
||||||
|
|
||||||
# The guiding principle of classes is SRP, SRP can't be accurately measured by LoC
|
# The guiding principle of classes is SRP, SRP can't be accurately measured by LoC
|
||||||
Metrics/ClassLength:
|
Metrics/ClassLength:
|
||||||
|
|
@ -41,9 +36,10 @@ Metrics/ModuleLength:
|
||||||
Metrics/AbcSize:
|
Metrics/AbcSize:
|
||||||
Exclude:
|
Exclude:
|
||||||
- "db/migrate/*"
|
- "db/migrate/*"
|
||||||
Max: 50
|
Max: 75
|
||||||
|
|
||||||
Metrics/BlockLength:
|
Metrics/BlockLength:
|
||||||
|
Max: 30
|
||||||
AllowedMethods:
|
AllowedMethods:
|
||||||
- "namespace"
|
- "namespace"
|
||||||
- "create_table"
|
- "create_table"
|
||||||
|
|
@ -51,6 +47,42 @@ Metrics/BlockLength:
|
||||||
- "config/**/*.rb"
|
- "config/**/*.rb"
|
||||||
- "spec/**/*.rb"
|
- "spec/**/*.rb"
|
||||||
|
|
||||||
|
Metrics/CyclomaticComplexity:
|
||||||
|
Max: 22
|
||||||
|
|
||||||
|
Metrics/ParameterLists:
|
||||||
|
Max: 7
|
||||||
|
|
||||||
|
Metrics/PerceivedComplexity:
|
||||||
|
Max: 24
|
||||||
|
|
||||||
|
Rails/ReversibleMigration:
|
||||||
|
Exclude:
|
||||||
|
- "db/migrate/**/*"
|
||||||
|
|
||||||
|
Rails/SkipsModelValidations:
|
||||||
|
Exclude:
|
||||||
|
- "app/models/tournament_sync_queue_entry.rb"
|
||||||
|
- "app/services/tournament_sync_pusher.rb"
|
||||||
|
- "db/migrate/**/*"
|
||||||
|
|
||||||
|
# These are standalone CLI entrypoints where stdout is command output,
|
||||||
|
# not Rails application logging.
|
||||||
|
Rails/Output:
|
||||||
|
Exclude:
|
||||||
|
- "e2e/load_test.rb"
|
||||||
|
- "script/e2e_scenarios.rb"
|
||||||
|
- "script/wait_for_http.rb"
|
||||||
|
|
||||||
|
Rails/Delegate:
|
||||||
|
Exclude:
|
||||||
|
- "app/controllers/tournaments_controller.rb"
|
||||||
|
- "e2e/lib/action_cable_client.rb"
|
||||||
|
|
||||||
|
Style/MultilineBlockChain:
|
||||||
|
Exclude:
|
||||||
|
- "e2e/lib/api_client.rb"
|
||||||
|
|
||||||
# Disable documentation checks for now
|
# Disable documentation checks for now
|
||||||
# should be done with yard instead
|
# should be done with yard instead
|
||||||
Style/Documentation:
|
Style/Documentation:
|
||||||
|
|
|
||||||
|
|
@ -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.ru Rakefile /app/
|
||||||
COPY config/environments/production.rb /app/config/environments/beta.rb
|
COPY config/environments/production.rb /app/config/environments/beta.rb
|
||||||
COPY tasks.py /app/tasks.py
|
COPY tasks.py /app/tasks.py
|
||||||
|
COPY .rubocop.yml /app/.rubocop.yml
|
||||||
COPY e2e /app/e2e
|
COPY e2e /app/e2e
|
||||||
COPY lib /app/lib
|
COPY lib /app/lib
|
||||||
COPY spec /app/spec
|
COPY spec /app/spec
|
||||||
|
|
|
||||||
4
Gemfile
4
Gemfile
|
|
@ -44,8 +44,8 @@ group :test, optional: true do
|
||||||
gem 'factory_bot_rails'
|
gem 'factory_bot_rails'
|
||||||
gem 'faker'
|
gem 'faker'
|
||||||
gem 'rspec-rails'
|
gem 'rspec-rails'
|
||||||
gem 'shoulda-matchers'
|
|
||||||
gem 'ruby-prof'
|
gem 'ruby-prof'
|
||||||
|
gem 'shoulda-matchers'
|
||||||
end
|
end
|
||||||
|
|
||||||
group :development, optional: true do
|
group :development, optional: true do
|
||||||
|
|
@ -53,13 +53,13 @@ group :development, optional: true do
|
||||||
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
|
# Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
|
||||||
gem 'railroady'
|
gem 'railroady'
|
||||||
# Use sqlite3 as the database for Active Record
|
# Use sqlite3 as the database for Active Record
|
||||||
gem 'sqlite3'
|
|
||||||
gem 'rubocop'
|
gem 'rubocop'
|
||||||
gem 'rubocop-performance'
|
gem 'rubocop-performance'
|
||||||
gem 'rubocop-rails'
|
gem 'rubocop-rails'
|
||||||
gem 'solargraph'
|
gem 'solargraph'
|
||||||
gem 'spring'
|
gem 'spring'
|
||||||
gem 'spring-watcher-listen', '~> 2.1'
|
gem 'spring-watcher-listen', '~> 2.1'
|
||||||
|
gem 'sqlite3'
|
||||||
end
|
end
|
||||||
|
|
||||||
group :production do
|
group :production do
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,8 @@ class MatchesController < ApplicationController
|
||||||
elsif match.save
|
elsif match.save
|
||||||
UpdateGroupsGroupScoresAndSave.call(group: @group)
|
UpdateGroupsGroupScoresAndSave.call(group: @group)
|
||||||
group_scores = group_scores_for_log(@group)
|
group_scores = group_scores_for_log(@group)
|
||||||
TournamentTransactionLogger.record_decider_match_created!(match: match, user: current_user, group_scores: group_scores)
|
TournamentTransactionLogger.record_decider_match_created!(match: match, user: current_user,
|
||||||
|
group_scores: group_scores)
|
||||||
synced_tournament = @group.tournament
|
synced_tournament = @group.tournament
|
||||||
render json: match, include: %w[match_scores.team group], status: :created
|
render json: match, include: %w[match_scores.team group], status: :created
|
||||||
else
|
else
|
||||||
|
|
@ -90,7 +91,8 @@ class MatchesController < ApplicationController
|
||||||
# PATCH /matches/1/swap
|
# PATCH /matches/1/swap
|
||||||
def swap
|
def swap
|
||||||
unless @match.swappable_with?(@swap_target)
|
unless @match.swappable_with?(@swap_target)
|
||||||
render json: { error: 'Matches must be different group matches from the same group' }, status: :unprocessable_content
|
render json: { error: 'Matches must be different group matches from the same group' },
|
||||||
|
status: :unprocessable_content
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -107,7 +109,8 @@ class MatchesController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
swapped_matches = Match.where(id: [@match.id, @swap_target.id]).order(:position)
|
swapped_matches = Match.where(id: [@match.id, @swap_target.id]).order(:position)
|
||||||
render json: swapped_matches, each_serializer: ExtendedMatchSerializer, include: %w[match_scores.team bets stage group]
|
render json: swapped_matches, each_serializer: ExtendedMatchSerializer,
|
||||||
|
include: %w[match_scores.team bets stage group]
|
||||||
push_sync_if_needed!(synced_tournament)
|
push_sync_if_needed!(synced_tournament)
|
||||||
TournamentLiveBroadcast.call(synced_tournament.id)
|
TournamentLiveBroadcast.call(synced_tournament.id)
|
||||||
end
|
end
|
||||||
|
|
@ -117,9 +120,7 @@ class MatchesController < ApplicationController
|
||||||
def handle_match_end
|
def handle_match_end
|
||||||
if @match.group_match?
|
if @match.group_match?
|
||||||
group = @match.group
|
group = @match.group
|
||||||
unless UpdateGroupsGroupScoresAndSave.call(group: group).success?
|
logger.warn "Updating groups group score failed for #{group}" unless UpdateGroupsGroupScoresAndSave.call(group: group).success?
|
||||||
logger.warn "Updating groups group score failed for #{group}"
|
|
||||||
end
|
|
||||||
@transaction_log_group_scores = group_scores_for_log(group)
|
@transaction_log_group_scores = group_scores_for_log(group)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
|
@ -290,9 +291,7 @@ class MatchesController < ApplicationController
|
||||||
decider: true,
|
decider: true,
|
||||||
match_scores: teams.map { |team| MatchScore.new(team: team) }
|
match_scores: teams.map { |team| MatchScore.new(team: team) }
|
||||||
).tap do |match|
|
).tap do |match|
|
||||||
if team_ids.size != 2 || teams.size != 2
|
match.errors.add(:team_ids, 'must contain exactly two teams from this group') if team_ids.size != 2 || teams.size != 2
|
||||||
match.errors.add(:team_ids, 'must contain exactly two teams from this group')
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,9 +71,7 @@ class TeamsController < ApplicationController
|
||||||
return [] if id_values.empty?
|
return [] if id_values.empty?
|
||||||
|
|
||||||
User.where(id: id_values).tap do |players|
|
User.where(id: id_values).tap do |players|
|
||||||
unless players.size == id_values.uniq.size
|
raise ActiveRecord::RecordNotFound.new('Could not find all players', 'User') unless players.size == id_values.uniq.size
|
||||||
raise ActiveRecord::RecordNotFound.new('Could not find all players', 'User')
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,8 @@ class TournamentsController < ApplicationController
|
||||||
before_action :set_tournament_for_show, only: %i[show]
|
before_action :set_tournament_for_show, only: %i[show]
|
||||||
before_action :set_tournament,
|
before_action :set_tournament,
|
||||||
only: %i[update destroy destroy_playoffs set_timer timer sync_state test_sync start_next_group_matches]
|
only: %i[update destroy destroy_playoffs set_timer timer sync_state test_sync start_next_group_matches]
|
||||||
before_action :authenticate_user!, only: %i[create update destroy destroy_playoffs set_timer test_sync start_next_group_matches]
|
before_action :authenticate_user!,
|
||||||
|
only: %i[create update destroy destroy_playoffs set_timer test_sync start_next_group_matches]
|
||||||
before_action -> { require_owner! @tournament.owner },
|
before_action -> { require_owner! @tournament.owner },
|
||||||
only: %i[update destroy destroy_playoffs set_timer test_sync start_next_group_matches]
|
only: %i[update destroy destroy_playoffs set_timer test_sync start_next_group_matches]
|
||||||
before_action :validate_create_params, only: %i[create]
|
before_action :validate_create_params, only: %i[create]
|
||||||
|
|
@ -74,6 +75,7 @@ class TournamentsController < ApplicationController
|
||||||
end
|
end
|
||||||
|
|
||||||
raise ActionController::ParameterMissing, :teams if teams.nil?
|
raise ActionController::ParameterMissing, :teams if teams.nil?
|
||||||
|
|
||||||
if group_stage
|
if group_stage
|
||||||
params.require(:playoff_teams_amount)
|
params.require(:playoff_teams_amount)
|
||||||
groups = organize_teams_in_groups(teams)
|
groups = organize_teams_in_groups(teams)
|
||||||
|
|
@ -339,7 +341,7 @@ class TournamentsController < ApplicationController
|
||||||
return if ActiveModel::Type::Boolean.new.cast(params[:read_only_mode])
|
return if ActiveModel::Type::Boolean.new.cast(params[:read_only_mode])
|
||||||
|
|
||||||
teams = params['teams']
|
teams = params['teams']
|
||||||
return if teams.is_a?(Array) && teams.reject { |t| t.is_a? ActionController::Parameters }.count.zero?
|
return if teams.is_a?(Array) && teams.count { |team| !team.is_a?(ActionController::Parameters) }.zero?
|
||||||
|
|
||||||
render json: { error: 'Invalid teams array' }, status: :unprocessable_content
|
render json: { error: 'Invalid teams array' }, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
@ -374,12 +376,16 @@ class TournamentsController < ApplicationController
|
||||||
error: 'playoff_teams_amount, instant_finalists_amount and intermediate_round_participants_amount don\'t match'
|
error: 'playoff_teams_amount, instant_finalists_amount and intermediate_round_participants_amount don\'t match'
|
||||||
}, status: :unprocessable_content
|
}, status: :unprocessable_content
|
||||||
end
|
end
|
||||||
|
|
||||||
def sync_state_params
|
def sync_state_params
|
||||||
params.require(:snapshot).permit!.to_h
|
params.require(:snapshot).permit!.to_h
|
||||||
end
|
end
|
||||||
|
|
||||||
def authorize_sync_request!
|
def authorize_sync_request!
|
||||||
return render json: { error: 'Tournament is not configured to accept sync pushes' }, status: :unprocessable_content unless @tournament.sync_accepts_push?
|
unless @tournament.sync_accepts_push?
|
||||||
|
return render json: { error: 'Tournament is not configured to accept sync pushes' },
|
||||||
|
status: :unprocessable_content
|
||||||
|
end
|
||||||
|
|
||||||
token = request.authorization.to_s.delete_prefix('Bearer ').presence || request.headers['X-Tournament-Sync-Token'].to_s
|
token = request.authorization.to_s.delete_prefix('Bearer ').presence || request.headers['X-Tournament-Sync-Token'].to_s
|
||||||
return render json: { error: 'Missing sync token' }, status: :unauthorized if token.blank?
|
return render json: { error: 'Missing sync token' }, status: :unauthorized if token.blank?
|
||||||
|
|
@ -425,13 +431,9 @@ class TournamentsController < ApplicationController
|
||||||
timer_reason_text = params[:timer_reason_text]
|
timer_reason_text = params[:timer_reason_text]
|
||||||
clear_timer = ActiveModel::Type::Boolean.new.cast(params[:clear_timer])
|
clear_timer = ActiveModel::Type::Boolean.new.cast(params[:clear_timer])
|
||||||
|
|
||||||
if clear_timer
|
return validate_clear_timer_params(timestamp, timestamp_seconds, timer_mode, timer_reason, timer_reason_text) if clear_timer
|
||||||
return validate_clear_timer_params(timestamp, timestamp_seconds, timer_mode, timer_reason, timer_reason_text)
|
|
||||||
end
|
|
||||||
|
|
||||||
if timer_mode.present? && !Tournament::TIMER_MODES.include?(timer_mode)
|
return render_timer_validation_error('Invalid timer_mode') if timer_mode.present? && !Tournament::TIMER_MODES.include?(timer_mode)
|
||||||
return render_timer_validation_error('Invalid timer_mode')
|
|
||||||
end
|
|
||||||
|
|
||||||
timer_reason_error = validate_timer_reason_params(
|
timer_reason_error = validate_timer_reason_params(
|
||||||
timer_reason,
|
timer_reason,
|
||||||
|
|
@ -441,7 +443,20 @@ class TournamentsController < ApplicationController
|
||||||
)
|
)
|
||||||
return render_timer_validation_error(timer_reason_error) if timer_reason_error
|
return render_timer_validation_error(timer_reason_error) if timer_reason_error
|
||||||
|
|
||||||
# throw error if both timestamp and timestamp_seconds are present
|
return if validate_timer_timestamp_params(timestamp, timestamp_seconds, timer_mode)
|
||||||
|
|
||||||
|
if timer_mode == 'countup'
|
||||||
|
params[:timestamp] = Time.zone.now
|
||||||
|
elsif timer_mode.present?
|
||||||
|
render_timer_validation_error('timer_mode requires timestamp, timestamp_seconds, or clear_timer')
|
||||||
|
elsif timer_reason_update_requested
|
||||||
|
render_timer_validation_error('timer_reason requires active timer') unless timer_active?
|
||||||
|
else
|
||||||
|
render_timer_validation_error('timestamp, timestamp_seconds, or clear_timer is required')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_timer_timestamp_params(timestamp, timestamp_seconds, timer_mode)
|
||||||
if timestamp.present? && timestamp_seconds.present?
|
if timestamp.present? && timestamp_seconds.present?
|
||||||
return render_timer_validation_error('Only one of timestamp or timestamp_seconds is allowed')
|
return render_timer_validation_error('Only one of timestamp or timestamp_seconds is allowed')
|
||||||
end
|
end
|
||||||
|
|
@ -450,22 +465,23 @@ class TournamentsController < ApplicationController
|
||||||
return render_timer_validation_error('timestamp_seconds is only allowed for countdown mode')
|
return render_timer_validation_error('timestamp_seconds is only allowed for countdown mode')
|
||||||
end
|
end
|
||||||
|
|
||||||
if timestamp_seconds.present?
|
return validate_timestamp_seconds_param(timestamp_seconds) if timestamp_seconds.present?
|
||||||
begin
|
return validate_timestamp_param(timestamp, timer_mode) if timestamp.present?
|
||||||
timestamp_seconds = Integer(timestamp_seconds)
|
|
||||||
rescue ArgumentError
|
false
|
||||||
return render_timer_validation_error('Invalid seconds format')
|
|
||||||
end
|
end
|
||||||
|
|
||||||
if timestamp_seconds <= 0
|
def validate_timestamp_seconds_param(timestamp_seconds)
|
||||||
return render_timer_validation_error('Timestamp must be in the future for countdown mode')
|
seconds = Integer(timestamp_seconds)
|
||||||
end
|
return render_timer_validation_error('Timestamp must be in the future for countdown mode') if seconds <= 0
|
||||||
|
|
||||||
parsed_time = Time.zone.now + timestamp_seconds
|
params[:timestamp] = Time.zone.now + seconds
|
||||||
params[:timestamp] = parsed_time
|
|
||||||
params[:timer_mode] = 'countdown'
|
params[:timer_mode] = 'countdown'
|
||||||
elsif timestamp.present?
|
rescue ArgumentError
|
||||||
begin
|
render_timer_validation_error('Invalid seconds format')
|
||||||
|
end
|
||||||
|
|
||||||
|
def validate_timestamp_param(timestamp, timer_mode)
|
||||||
parsed_time = Time.zone.parse(timestamp)
|
parsed_time = Time.zone.parse(timestamp)
|
||||||
if parsed_time.nil?
|
if parsed_time.nil?
|
||||||
return render_timer_validation_error('Invalid timestamp format')
|
return render_timer_validation_error('Invalid timestamp format')
|
||||||
|
|
@ -476,17 +492,7 @@ class TournamentsController < ApplicationController
|
||||||
params[:timestamp] = parsed_time
|
params[:timestamp] = parsed_time
|
||||||
params[:timer_mode] = timer_mode.presence || 'countdown'
|
params[:timer_mode] = timer_mode.presence || 'countdown'
|
||||||
rescue ArgumentError
|
rescue ArgumentError
|
||||||
return render_timer_validation_error('Invalid timestamp format')
|
render_timer_validation_error('Invalid timestamp format')
|
||||||
end
|
|
||||||
elsif timer_mode == 'countup'
|
|
||||||
params[:timestamp] = Time.zone.now
|
|
||||||
elsif timer_mode.present?
|
|
||||||
return render_timer_validation_error('timer_mode requires timestamp, timestamp_seconds, or clear_timer')
|
|
||||||
elsif timer_reason_update_requested
|
|
||||||
return render_timer_validation_error('timer_reason requires active timer') unless timer_active?
|
|
||||||
else
|
|
||||||
return render_timer_validation_error('timestamp, timestamp_seconds, or clear_timer is required')
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def normalize_timer_reason_params
|
def normalize_timer_reason_params
|
||||||
|
|
@ -518,17 +524,13 @@ class TournamentsController < ApplicationController
|
||||||
timer_reason_param_present,
|
timer_reason_param_present,
|
||||||
timer_reason_text_param_present
|
timer_reason_text_param_present
|
||||||
)
|
)
|
||||||
if timer_reason_text_param_present && !timer_reason_param_present
|
return 'timer_reason is required when timer_reason_text is provided' if timer_reason_text_param_present && !timer_reason_param_present
|
||||||
return 'timer_reason is required when timer_reason_text is provided'
|
|
||||||
end
|
|
||||||
return if timer_reason.blank? && timer_reason_text.blank?
|
return if timer_reason.blank? && timer_reason_text.blank?
|
||||||
|
|
||||||
return 'timer_reason_text is only allowed when timer_reason is custom' if timer_reason.blank?
|
return 'timer_reason_text is only allowed when timer_reason is custom' if timer_reason.blank?
|
||||||
return 'Invalid timer_reason' unless Tournament::TIMER_REASONS.include?(timer_reason)
|
return 'Invalid timer_reason' unless Tournament::TIMER_REASONS.include?(timer_reason)
|
||||||
|
|
||||||
if timer_reason == 'custom' && timer_reason_text.blank?
|
return 'timer_reason_text is required for custom timer_reason' if timer_reason == 'custom' && timer_reason_text.blank?
|
||||||
return 'timer_reason_text is required for custom timer_reason'
|
|
||||||
end
|
|
||||||
|
|
||||||
return unless timer_reason != 'custom' && timer_reason_text.present?
|
return unless timer_reason != 'custom' && timer_reason_text.present?
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@ class AdvanceTeamsInIntermediateStage
|
||||||
# populate stage below with the "winners" from single team matches
|
# populate stage below with the "winners" from single team matches
|
||||||
matches.select { |m| m.state == 'single_team' }
|
matches.select { |m| m.state == 'single_team' }
|
||||||
.each do |match|
|
.each do |match|
|
||||||
context.fail! unless PopulateMatchBelowAndSave.call(match: match).success?
|
context.fail! unless PopulateMatchBelowAndSave.call(match:).success?
|
||||||
end
|
end
|
||||||
(context.object_to_save ||= []) << intermediate_stage
|
(context.object_to_save ||= []) << intermediate_stage
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,9 @@ class Stage < ApplicationRecord
|
||||||
delegate :owner, to: :tournament
|
delegate :owner, to: :tournament
|
||||||
|
|
||||||
def teams
|
def teams
|
||||||
if !matches.size.zero?
|
if !matches.empty?
|
||||||
matches.map(&:teams).flatten.uniq
|
matches.map(&:teams).flatten.uniq
|
||||||
elsif !groups.size.zero?
|
elsif !groups.empty?
|
||||||
groups.map(&:teams).flatten.uniq
|
groups.map(&:teams).flatten.uniq
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
|
|
@ -20,9 +20,9 @@ class Stage < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def over?
|
def over?
|
||||||
return matches.find { |m| m.state != 'finished' }.nil? unless matches.size.zero?
|
return matches.find { |m| m.state != 'finished' }.nil? unless matches.empty?
|
||||||
|
|
||||||
unless groups.size.zero? && groups.map(&:matches).flatten.size.zero?
|
unless groups.empty? && groups.map(&:matches).flatten.empty?
|
||||||
relevant_matches = groups.map(&:matches).flatten.reject(&:decider?)
|
relevant_matches = groups.map(&:matches).flatten.reject(&:decider?)
|
||||||
return relevant_matches.find { |m| m.state != 'finished' }.nil?
|
return relevant_matches.find { |m| m.state != 'finished' }.nil?
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@ class TeamActionList < ApplicationRecord
|
||||||
|
|
||||||
validates :name, :key, :action_name, :source, presence: true
|
validates :name, :key, :action_name, :source, presence: true
|
||||||
validates :key, uniqueness: { scope: :tournament_id }
|
validates :key, uniqueness: { scope: :tournament_id }
|
||||||
validates :key, format: { with: /\A[a-z0-9_]+\z/, message: 'may only contain lowercase letters, numbers, and underscores' }
|
validates :key,
|
||||||
|
format: { with: /\A[a-z0-9_]+\z/, message: 'may only contain lowercase letters, numbers, and underscores' }
|
||||||
|
|
||||||
delegate :owner, to: :tournament
|
delegate :owner, to: :tournament
|
||||||
|
|
||||||
|
|
@ -20,7 +21,7 @@ class TeamActionList < ApplicationRecord
|
||||||
team_action_items.where.not(team_id: desired_team_ids).destroy_all
|
team_action_items.where.not(team_id: desired_team_ids).destroy_all
|
||||||
|
|
||||||
desired_team_ids.each do |team_id|
|
desired_team_ids.each do |team_id|
|
||||||
team_action_items.find_or_create_by!(team_id: team_id)
|
team_action_items.find_or_create_by!(team_id:)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ class Tournament < ApplicationRecord
|
||||||
METADATA_STRING_ATTRIBUTES = %i[location website_url].freeze
|
METADATA_STRING_ATTRIBUTES = %i[location website_url].freeze
|
||||||
|
|
||||||
belongs_to :user
|
belongs_to :user
|
||||||
belongs_to :owner, class_name: 'User', foreign_key: :user_id
|
belongs_to :owner, class_name: 'User', foreign_key: :user_id, inverse_of: :tournaments
|
||||||
has_many :teams, dependent: :destroy
|
has_many :teams, dependent: :destroy
|
||||||
has_many :stages, dependent: :destroy
|
has_many :stages, dependent: :destroy
|
||||||
has_many :beamers, dependent: :destroy
|
has_many :beamers, dependent: :destroy
|
||||||
|
|
|
||||||
|
|
@ -45,9 +45,9 @@ class TournamentSyncQueueEntry < ApplicationRecord
|
||||||
def acquire_lock!
|
def acquire_lock!
|
||||||
token = SecureRandom.hex(8)
|
token = SecureRandom.hex(8)
|
||||||
updated = self.class
|
updated = self.class
|
||||||
.where(id: id)
|
.where(id:)
|
||||||
.where(locked_at: nil)
|
.where(locked_at: nil)
|
||||||
.or(self.class.where(id: id).where('locked_at < ?', Time.current - LOCK_TTL))
|
.or(self.class.where(id:).where('locked_at < ?', Time.current - LOCK_TTL))
|
||||||
.update_all(locked_at: Time.current, lock_token: token)
|
.update_all(locked_at: Time.current, lock_token: token)
|
||||||
return nil if updated.zero?
|
return nil if updated.zero?
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,18 +3,18 @@
|
||||||
class GroupStageService
|
class GroupStageService
|
||||||
class << self
|
class << self
|
||||||
def generate_group_stage(groups)
|
def generate_group_stage(groups)
|
||||||
raise 'Cannot generate group stage without groups' if groups.length.zero?
|
raise 'Cannot generate group stage without groups' if groups.empty?
|
||||||
|
|
||||||
# raise an error if the average group size is not a whole number
|
# raise an error if the average group size is not a whole number
|
||||||
raise 'Groups need to be equal size' unless (groups.flatten.length.to_f / groups.length % 1).zero?
|
raise 'Groups need to be equal size' unless (groups.flatten.length.to_f / groups.length % 1).zero?
|
||||||
|
|
||||||
groups = groups.map(&method(:get_group_object_from)).each_with_index { |group, i| group.number = i + 1 }
|
groups = groups.map(&method(:get_group_object_from)).each_with_index { |group, i| group.number = i + 1 }
|
||||||
Stage.new level: -1, groups: groups, state: :in_progress
|
Stage.new level: -1, groups:, state: :in_progress
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_group_object_from(team_array)
|
def get_group_object_from(team_array)
|
||||||
Group.new matches: generate_all_matches_between(team_array),
|
Group.new matches: generate_all_matches_between(team_array),
|
||||||
group_scores: team_array.map { |team| GroupScore.new team: team }
|
group_scores: team_array.map { |team| GroupScore.new team: }
|
||||||
end
|
end
|
||||||
|
|
||||||
def deal_with_spacing_of_teams(matches, team_size)
|
def deal_with_spacing_of_teams(matches, team_size)
|
||||||
|
|
@ -71,7 +71,7 @@ class GroupStageService
|
||||||
def update_group_scores(group)
|
def update_group_scores(group)
|
||||||
changed_group_scores = []
|
changed_group_scores = []
|
||||||
group.teams.each do |team|
|
group.teams.each do |team|
|
||||||
group_score = group.group_scores.find_by(team: team)
|
group_score = group.group_scores.find_by(team:)
|
||||||
matches = group.matches.select { |match| match.teams.include? team }
|
matches = group.matches.select { |match| match.teams.include? team }
|
||||||
# reset previous values
|
# reset previous values
|
||||||
group_score.group_points = 0
|
group_score.group_points = 0
|
||||||
|
|
@ -239,11 +239,9 @@ class GroupStageService
|
||||||
start_index -= 1 while start_index.positive? && sorted_group_scores[start_index - 1].position == position
|
start_index -= 1 while start_index.positive? && sorted_group_scores[start_index - 1].position == position
|
||||||
|
|
||||||
end_index = index
|
end_index = index
|
||||||
while end_index < sorted_group_scores.size - 1 && sorted_group_scores[end_index + 1].position == position
|
end_index += 1 while end_index < sorted_group_scores.size - 1 && sorted_group_scores[end_index + 1].position == position
|
||||||
end_index += 1
|
|
||||||
end
|
|
||||||
|
|
||||||
{ start_index: start_index, end_index: end_index }
|
{ start_index:, end_index: }
|
||||||
end
|
end
|
||||||
|
|
||||||
def advancing_slots_for_group(group)
|
def advancing_slots_for_group(group)
|
||||||
|
|
@ -272,7 +270,7 @@ class GroupStageService
|
||||||
advancing_slots = advancing_slots_for_group(group)
|
advancing_slots = advancing_slots_for_group(group)
|
||||||
group_ready_for_decider_check = regular_group_matches_finished?(group)
|
group_ready_for_decider_check = regular_group_matches_finished?(group)
|
||||||
|
|
||||||
cache = sorted_group_scores.each_with_index.each_with_object({}) do |(group_score, index), decisions|
|
cache = sorted_group_scores.each_with_index.with_object({}) do |(group_score, index), decisions|
|
||||||
next if sorted_group_scores.size <= 1
|
next if sorted_group_scores.size <= 1
|
||||||
|
|
||||||
comparison_index = comparison_partner_index(sorted_group_scores, index)
|
comparison_index = comparison_partner_index(sorted_group_scores, index)
|
||||||
|
|
@ -297,7 +295,8 @@ class GroupStageService
|
||||||
tied: comparison_reason[:tied],
|
tied: comparison_reason[:tied],
|
||||||
needs_decider_match: group_ready_for_decider_check &&
|
needs_decider_match: group_ready_for_decider_check &&
|
||||||
comparison_reason[:tied] &&
|
comparison_reason[:tied] &&
|
||||||
unresolved_tie_affects_advancing_placements?(sorted_group_scores, group_score, advancing_slots)
|
unresolved_tie_affects_advancing_placements?(sorted_group_scores, group_score,
|
||||||
|
advancing_slots)
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ class PlayoffStageService
|
||||||
initial_stage.state = :intermediate_stage unless initial_stage.matches.find(&:single_team?).nil?
|
initial_stage.state = :intermediate_stage unless initial_stage.matches.find(&:single_team?).nil?
|
||||||
playoffs << initial_stage
|
playoffs << initial_stage
|
||||||
# empty stages are the stages, the tournament is filled with to have the matches ready for later
|
# empty stages are the stages, the tournament is filled with to have the matches ready for later
|
||||||
empty_stages = generate_stages_with_empty_matches(stage_count - 1, teams.size > 3)
|
empty_stages = generate_stages_with_empty_matches(stage_count - 1, include_third_place_match: teams.size > 3)
|
||||||
playoffs.concat empty_stages
|
playoffs.concat empty_stages
|
||||||
playoffs
|
playoffs
|
||||||
end
|
end
|
||||||
|
|
@ -25,7 +25,7 @@ class PlayoffStageService
|
||||||
#
|
#
|
||||||
# @param stage_count [Integer] number of stages to generate
|
# @param stage_count [Integer] number of stages to generate
|
||||||
# @return [Array] the generated stages
|
# @return [Array] the generated stages
|
||||||
def generate_stages_with_empty_matches(stage_count, include_third_place_match = false)
|
def generate_stages_with_empty_matches(stage_count, include_third_place_match: false)
|
||||||
empty_stages = []
|
empty_stages = []
|
||||||
stage_count.times do |i|
|
stage_count.times do |i|
|
||||||
match_count = 2**i
|
match_count = 2**i
|
||||||
|
|
@ -82,7 +82,9 @@ class PlayoffStageService
|
||||||
# of which the winners advance into the match below
|
# of which the winners advance into the match below
|
||||||
# depending on the position of the match, the companion match is either on the left or right of it
|
# depending on the position of the match, the companion match is either on the left or right of it
|
||||||
companion_match = find_companion_match(current_match.position, current_stage)
|
companion_match = find_companion_match(current_match.position, current_stage)
|
||||||
return populate_final_stage_matches(current_match, companion_match, next_stage) if semifinal_match?(current_stage, next_stage)
|
return populate_final_stage_matches(current_match, companion_match, next_stage) if semifinal_match?(
|
||||||
|
current_stage, next_stage
|
||||||
|
)
|
||||||
|
|
||||||
winners = get_winners_of(companion_match, current_match)
|
winners = get_winners_of(companion_match, current_match)
|
||||||
populate_match!(match: next_stage.matches.find { |m| m.position == current_match.position / 2 }, teams: winners)
|
populate_match!(match: next_stage.matches.find { |m| m.position == current_match.position / 2 }, teams: winners)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
class TeamActionItemUpdate
|
class TeamActionItemUpdate
|
||||||
class << self
|
class << self
|
||||||
def call(team_action_item:, completed:)
|
def call(team_action_item:, completed:)
|
||||||
new(team_action_item: team_action_item, completed: completed).call
|
new(team_action_item:, completed:).call
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -13,7 +13,7 @@ class TeamActionItemUpdate
|
||||||
end
|
end
|
||||||
|
|
||||||
def call
|
def call
|
||||||
return false unless team_action_item.update(completed: completed)
|
return false unless team_action_item.update(completed:)
|
||||||
|
|
||||||
TournamentSyncEnqueue.call(team_action_item.tournament)
|
TournamentSyncEnqueue.call(team_action_item.tournament)
|
||||||
true
|
true
|
||||||
|
|
|
||||||
|
|
@ -23,9 +23,7 @@ class TournamentPlayoffDeletion
|
||||||
|
|
||||||
playoff_stages = tournament.stages.reload.select { |stage| stage.level >= 0 }
|
playoff_stages = tournament.stages.reload.select { |stage| stage.level >= 0 }
|
||||||
return failure('Playoffs have not been generated') if playoff_stages.empty?
|
return failure('Playoffs have not been generated') if playoff_stages.empty?
|
||||||
unless playoffs_deletable?(playoff_stages)
|
return failure('Playoffs cannot be deleted after matches have started or scores changed') unless playoffs_deletable?(playoff_stages)
|
||||||
return failure('Playoffs cannot be deleted after matches have started or scores changed')
|
|
||||||
end
|
|
||||||
|
|
||||||
playoff_stages.each(&:destroy!)
|
playoff_stages.each(&:destroy!)
|
||||||
group_stage.update!(state: :in_progress)
|
group_stage.update!(state: :in_progress)
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ class TournamentSnapshotImporter
|
||||||
].freeze
|
].freeze
|
||||||
|
|
||||||
def self.import!(tournament:, snapshot:)
|
def self.import!(tournament:, snapshot:)
|
||||||
new(tournament: tournament, snapshot: snapshot).import!
|
new(tournament:, snapshot:).import!
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(tournament:, snapshot:)
|
def initialize(tournament:, snapshot:)
|
||||||
|
|
@ -222,6 +222,7 @@ class TournamentSnapshotImporter
|
||||||
stale_records = existing_records.reject { |record| source_ids.include?(record.sync_source_id) }
|
stale_records = existing_records.reject { |record| source_ids.include?(record.sync_source_id) }
|
||||||
stale_records.each(&:destroy!)
|
stale_records.each(&:destroy!)
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse_time(value)
|
def parse_time(value)
|
||||||
return nil if value.blank?
|
return nil if value.blank?
|
||||||
|
|
||||||
|
|
@ -242,6 +243,6 @@ class TournamentSnapshotImporter
|
||||||
def code_taken_by_another_tournament?(code)
|
def code_taken_by_another_tournament?(code)
|
||||||
return false if code.blank?
|
return false if code.blank?
|
||||||
|
|
||||||
Tournament.where(code: code).where.not(id: tournament.id).exists?
|
Tournament.where(code:).where.not(id: tournament.id).exists?
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ class TournamentSyncEnqueue
|
||||||
return false unless tournament.sync_push_enabled?
|
return false unless tournament.sync_push_enabled?
|
||||||
|
|
||||||
payload = TournamentSnapshotBuilder.build(fresh_tournament)
|
payload = TournamentSnapshotBuilder.build(fresh_tournament)
|
||||||
entry = TournamentSyncQueueEntry.find_or_initialize_by(tournament: tournament)
|
entry = TournamentSyncQueueEntry.find_or_initialize_by(tournament:)
|
||||||
entry.snapshot_json = JSON.dump(payload)
|
entry.snapshot_json = JSON.dump(payload)
|
||||||
entry.status = 'pending'
|
entry.status = 'pending'
|
||||||
entry.attempts = 0
|
entry.attempts = 0
|
||||||
|
|
|
||||||
|
|
@ -27,11 +27,9 @@ class TournamentSyncPusher
|
||||||
end
|
end
|
||||||
|
|
||||||
def push_snapshot!(snapshot)
|
def push_snapshot!(snapshot)
|
||||||
response = perform_request(snapshot: snapshot)
|
response = perform_request(snapshot:)
|
||||||
|
|
||||||
unless response.is_a?(Net::HTTPSuccess)
|
raise SyncFailed, "sync push failed with status #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
|
||||||
raise SyncFailed, "sync push failed with status #{response.code}: #{response.body}"
|
|
||||||
end
|
|
||||||
|
|
||||||
tournament.update_columns(sync_last_pushed_at: Time.current, sync_last_push_error: nil)
|
tournament.update_columns(sync_last_pushed_at: Time.current, sync_last_push_error: nil)
|
||||||
true
|
true
|
||||||
|
|
@ -53,7 +51,7 @@ class TournamentSyncPusher
|
||||||
request['accept'] = 'application/json'
|
request['accept'] = 'application/json'
|
||||||
request['content-type'] = 'application/json'
|
request['content-type'] = 'application/json'
|
||||||
request['authorization'] = "Bearer #{tournament.sync_auth_token}"
|
request['authorization'] = "Bearer #{tournament.sync_auth_token}"
|
||||||
request.body = JSON.dump({ snapshot: snapshot })
|
request.body = JSON.dump({ snapshot: })
|
||||||
|
|
||||||
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
|
||||||
timeout = http_timeout_seconds
|
timeout = http_timeout_seconds
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,7 @@ class TournamentTransactionLogger
|
||||||
def lock_tournament!(tournament)
|
def lock_tournament!(tournament)
|
||||||
raise ArgumentError, 'tournament is required' if tournament.nil?
|
raise ArgumentError, 'tournament is required' if tournament.nil?
|
||||||
|
|
||||||
unless ActiveRecord::Base.connection.transaction_open?
|
raise 'Tournament transaction log writes must happen inside a transaction' unless ActiveRecord::Base.connection.transaction_open?
|
||||||
raise 'Tournament transaction log writes must happen inside a transaction'
|
|
||||||
end
|
|
||||||
|
|
||||||
# Lock a fresh row instance so callers keep saved_change_to_* state for
|
# Lock a fresh row instance so callers keep saved_change_to_* state for
|
||||||
# after_commit callbacks on the object they just updated.
|
# after_commit callbacks on the object they just updated.
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ class UserService
|
||||||
|
|
||||||
def bet!(match, team)
|
def bet!(match, team)
|
||||||
validate_bet! match, team
|
validate_bet! match, team
|
||||||
@user.bets.create! match: match, team: team
|
@user.bets.create! match:, team:
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,5 @@ module TurniereBackend
|
||||||
config.action_mailer.default_options = {
|
config.action_mailer.default_options = {
|
||||||
from: 'noreply@turnie.re'
|
from: 'noreply@turnie.re'
|
||||||
}
|
}
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require "bundler/setup" # Set up gems listed in the Gemfile.
|
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)
|
||||||
require "bootsnap/setup" # Speed up boot time by caching expensive operations.
|
|
||||||
|
require 'bundler/setup' # Set up gems listed in the Gemfile.
|
||||||
|
require 'bootsnap/setup' # Speed up boot time by caching expensive operations.
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ Rails.application.configure do
|
||||||
|
|
||||||
# Enable/disable caching. By default caching is disabled.
|
# Enable/disable caching. By default caching is disabled.
|
||||||
# Run rails dev:cache to toggle caching.
|
# Run rails dev:cache to toggle caching.
|
||||||
if Rails.root.join('tmp', 'caching-dev.txt').exist?
|
if Rails.root.join('tmp/caching-dev.txt').exist?
|
||||||
config.action_controller.perform_caching = true
|
config.action_controller.perform_caching = true
|
||||||
|
|
||||||
config.cache_store = :memory_store
|
config.cache_store = :memory_store
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,7 @@ Rails.application.configure do
|
||||||
config.active_support.deprecation = :notify
|
config.active_support.deprecation = :notify
|
||||||
|
|
||||||
# Use default logging formatter so that PID and timestamp are not suppressed.
|
# Use default logging formatter so that PID and timestamp are not suppressed.
|
||||||
config.log_formatter = ::Logger::Formatter.new
|
config.log_formatter = Logger::Formatter.new
|
||||||
|
|
||||||
# Use a different logger for distributed setups.
|
# Use a different logger for distributed setups.
|
||||||
# require 'syslog/logger'
|
# require 'syslog/logger'
|
||||||
|
|
@ -91,5 +91,5 @@ Rails.application.configure do
|
||||||
end
|
end
|
||||||
|
|
||||||
# Logging to STDOUT (running in docker by default)
|
# Logging to STDOUT (running in docker by default)
|
||||||
config.logger = Logger.new(STDOUT)
|
config.logger = Logger.new($stdout)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -14,14 +14,14 @@ module DeviseRoutesRails8Compat
|
||||||
|
|
||||||
def devise_password(mapping, controllers)
|
def devise_password(mapping, controllers)
|
||||||
resource :password,
|
resource :password,
|
||||||
only: [:new, :create, :edit, :update],
|
only: %i[new create edit update],
|
||||||
path: mapping.path_names[:password],
|
path: mapping.path_names[:password],
|
||||||
controller: controllers[:passwords]
|
controller: controllers[:passwords]
|
||||||
end
|
end
|
||||||
|
|
||||||
def devise_confirmation(mapping, controllers)
|
def devise_confirmation(mapping, controllers)
|
||||||
resource :confirmation,
|
resource :confirmation,
|
||||||
only: [:new, :create, :show],
|
only: %i[new create show],
|
||||||
path: mapping.path_names[:confirmation],
|
path: mapping.path_names[:confirmation],
|
||||||
controller: controllers[:confirmations]
|
controller: controllers[:confirmations]
|
||||||
end
|
end
|
||||||
|
|
@ -30,14 +30,14 @@ module DeviseRoutesRails8Compat
|
||||||
return unless mapping.to.unlock_strategy_enabled?(:email)
|
return unless mapping.to.unlock_strategy_enabled?(:email)
|
||||||
|
|
||||||
resource :unlock,
|
resource :unlock,
|
||||||
only: [:new, :create, :show],
|
only: %i[new create show],
|
||||||
path: mapping.path_names[:unlock],
|
path: mapping.path_names[:unlock],
|
||||||
controller: controllers[:unlocks]
|
controller: controllers[:unlocks]
|
||||||
end
|
end
|
||||||
|
|
||||||
def devise_registration(mapping, controllers)
|
def devise_registration(mapping, controllers)
|
||||||
resource :registration,
|
resource :registration,
|
||||||
only: [:new, :create, :edit, :update, :destroy],
|
only: %i[new create edit update destroy],
|
||||||
path: mapping.path_names[:registration],
|
path: mapping.path_names[:registration],
|
||||||
path_names: {
|
path_names: {
|
||||||
new: mapping.path_names[:sign_up],
|
new: mapping.path_names[:sign_up],
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,10 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# Be sure to restart your server when you modify this file.
|
# Be sure to restart your server when you modify this file.
|
||||||
|
|
||||||
# Configure parameters to be filtered from the log file. Use this to limit dissemination of
|
# Configure parameters to be filtered from the log file. Use this to limit dissemination of
|
||||||
# sensitive information. See the ActiveSupport::ParameterFilter documentation for supported
|
# sensitive information. See the ActiveSupport::ParameterFilter documentation for supported
|
||||||
# notations and behaviors.
|
# notations and behaviors.
|
||||||
Rails.application.config.filter_parameters += [
|
Rails.application.config.filter_parameters += %i[
|
||||||
:passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn
|
passw secret token _key crypt salt certificate otp ssn
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# Be sure to restart your server when you modify this file.
|
# Be sure to restart your server when you modify this file.
|
||||||
#
|
#
|
||||||
# This file eases your Rails 7.0 framework defaults upgrade.
|
# This file eases your Rails 7.0 framework defaults upgrade.
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,9 @@
|
||||||
].each { |path| Spring.watch(path) }
|
].each { |path| Spring.watch(path) }
|
||||||
|
|
||||||
Spring.after_fork do
|
Spring.after_fork do
|
||||||
if ENV['DEBUGGER_STORED_RUBYLIB']
|
ENV['DEBUGGER_STORED_RUBYLIB']&.split(File::PATH_SEPARATOR)&.each do |path|
|
||||||
ENV['DEBUGGER_STORED_RUBYLIB'].split(File::PATH_SEPARATOR).each do |path|
|
next unless path.include?('ruby-debug-ide')
|
||||||
next unless path =~ /ruby-debug-ide/
|
|
||||||
load path + '/ruby-debug-ide/multiprocess/starter.rb'
|
load "#{path}/ruby-debug-ide/multiprocess/starter.rb"
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,20 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# This migration comes from active_storage (originally 20190112182829)
|
# This migration comes from active_storage (originally 20190112182829)
|
||||||
class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0]
|
class AddServiceNameToActiveStorageBlobs < ActiveRecord::Migration[6.0]
|
||||||
def up
|
def up
|
||||||
return unless table_exists?(:active_storage_blobs)
|
return unless table_exists?(:active_storage_blobs)
|
||||||
|
|
||||||
unless column_exists?(:active_storage_blobs, :service_name)
|
return if column_exists?(:active_storage_blobs, :service_name)
|
||||||
|
|
||||||
add_column :active_storage_blobs, :service_name, :string
|
add_column :active_storage_blobs, :service_name, :string
|
||||||
|
|
||||||
if configured_service = ActiveStorage::Blob.service.name
|
if (configured_service = ActiveStorage::Blob.service.name)
|
||||||
ActiveStorage::Blob.unscoped.update_all(service_name: configured_service)
|
ActiveStorage::Blob.unscoped.update_all(service_name: configured_service)
|
||||||
end
|
end
|
||||||
|
|
||||||
change_column :active_storage_blobs, :service_name, :string, null: false
|
change_column :active_storage_blobs, :service_name, :string, null: false
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
def down
|
def down
|
||||||
return unless table_exists?(:active_storage_blobs)
|
return unless table_exists?(:active_storage_blobs)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# This migration comes from active_storage (originally 20191206030411)
|
# This migration comes from active_storage (originally 20191206030411)
|
||||||
class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0]
|
class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0]
|
||||||
def change
|
def change
|
||||||
|
|
@ -8,12 +10,13 @@ class CreateActiveStorageVariantRecords < ActiveRecord::Migration[6.0]
|
||||||
t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type
|
t.belongs_to :blob, null: false, index: false, type: blobs_primary_key_type
|
||||||
t.string :variation_digest, null: false
|
t.string :variation_digest, null: false
|
||||||
|
|
||||||
t.index %i[ blob_id variation_digest ], name: "index_active_storage_variant_records_uniqueness", unique: true
|
t.index %i[blob_id variation_digest], name: 'index_active_storage_variant_records_uniqueness', unique: true
|
||||||
t.foreign_key :active_storage_blobs, column: :blob_id
|
t.foreign_key :active_storage_blobs, column: :blob_id
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def primary_key_type
|
def primary_key_type
|
||||||
config = Rails.configuration.generators
|
config = Rails.configuration.generators
|
||||||
config.options[config.orm][:primary_key_type] || :primary_key
|
config.options[config.orm][:primary_key_type] || :primary_key
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# This migration comes from active_storage (originally 20211119233751)
|
# This migration comes from active_storage (originally 20211119233751)
|
||||||
class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0]
|
class RemoveNotNullOnActiveStorageBlobsChecksum < ActiveRecord::Migration[6.0]
|
||||||
def change
|
def change
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class AddPositionToGroupScores < ActiveRecord::Migration[7.0]
|
class AddPositionToGroupScores < ActiveRecord::Migration[7.0]
|
||||||
def change
|
def change
|
||||||
add_column :group_scores, :position, :integer
|
add_column :group_scores, :position, :integer
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class AddDefaultToPositionInGroupScores < ActiveRecord::Migration[7.0]
|
class AddDefaultToPositionInGroupScores < ActiveRecord::Migration[7.0]
|
||||||
def change
|
def change
|
||||||
change_column_default :group_scores, :position, 0
|
change_column_default :group_scores, :position, 0
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class AddTimerEndToTournaments < ActiveRecord::Migration[7.0]
|
class AddTimerEndToTournaments < ActiveRecord::Migration[7.0]
|
||||||
def change
|
def change
|
||||||
add_column :tournaments, :timer_end, :datetime
|
add_column :tournaments, :timer_end, :datetime
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ class CreateTeamActionLists < ActiveRecord::Migration[7.0]
|
||||||
t.timestamps
|
t.timestamps
|
||||||
end
|
end
|
||||||
|
|
||||||
add_index :team_action_lists, [:tournament_id, :key], unique: true
|
add_index :team_action_lists, %i[tournament_id key], unique: true
|
||||||
|
|
||||||
create_table :team_action_items do |t|
|
create_table :team_action_items do |t|
|
||||||
t.references :team_action_list, null: false, foreign_key: { on_delete: :cascade }, index: true
|
t.references :team_action_list, null: false, foreign_key: { on_delete: :cascade }, index: true
|
||||||
|
|
@ -25,6 +25,6 @@ class CreateTeamActionLists < ActiveRecord::Migration[7.0]
|
||||||
t.timestamps
|
t.timestamps
|
||||||
end
|
end
|
||||||
|
|
||||||
add_index :team_action_items, [:team_action_list_id, :team_id], unique: true
|
add_index :team_action_items, %i[team_action_list_id team_id], unique: true
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class AddTimerModeToTournaments < ActiveRecord::Migration[7.0]
|
class AddTimerModeToTournaments < ActiveRecord::Migration[7.0]
|
||||||
def change
|
def change
|
||||||
add_column :tournaments, :timer_mode, :string
|
add_column :tournaments, :timer_mode, :string
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,7 @@ class AddSyncAuthTokenDigestToTournaments < ActiveRecord::Migration[7.0]
|
||||||
end
|
end
|
||||||
|
|
||||||
def up
|
def up
|
||||||
unless column_exists?(:tournaments, :sync_auth_token_digest)
|
add_column :tournaments, :sync_auth_token_digest, :string unless column_exists?(:tournaments, :sync_auth_token_digest)
|
||||||
add_column :tournaments, :sync_auth_token_digest, :string
|
|
||||||
end
|
|
||||||
|
|
||||||
MigrationTournament.reset_column_information
|
MigrationTournament.reset_column_information
|
||||||
MigrationTournament
|
MigrationTournament
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ class CreateTournamentTransactionLogEntries < ActiveRecord::Migration[7.0]
|
||||||
end
|
end
|
||||||
|
|
||||||
add_index :tournament_transaction_log_entries,
|
add_index :tournament_transaction_log_entries,
|
||||||
[:tournament_id, :sequence],
|
%i[tournament_id sequence],
|
||||||
unique: true,
|
unique: true,
|
||||||
name: 'index_tournament_log_entries_on_tournament_and_sequence'
|
name: 'index_tournament_log_entries_on_tournament_and_sequence'
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ module TurniereE2E
|
||||||
ACTION_CABLE_PROTOCOLS = %w[actioncable-v1-json actioncable-unsupported].freeze
|
ACTION_CABLE_PROTOCOLS = %w[actioncable-v1-json actioncable-unsupported].freeze
|
||||||
|
|
||||||
def initialize(base_url:)
|
def initialize(base_url:)
|
||||||
@base_url = base_url.sub(%r{/\z}, '')
|
@base_url = base_url.delete_suffix('/')
|
||||||
@messages = Queue.new
|
@messages = Queue.new
|
||||||
@closed = false
|
@closed = false
|
||||||
end
|
end
|
||||||
|
|
@ -19,7 +19,7 @@ module TurniereE2E
|
||||||
def subscribe_team_action_lists!(tournament_id:)
|
def subscribe_team_action_lists!(tournament_id:)
|
||||||
identifier = {
|
identifier = {
|
||||||
channel: 'TournamentTeamActionListsChannel',
|
channel: 'TournamentTeamActionListsChannel',
|
||||||
tournament_id: tournament_id
|
tournament_id:
|
||||||
}.to_json
|
}.to_json
|
||||||
|
|
||||||
subscribe!(identifier:) do |message|
|
subscribe!(identifier:) do |message|
|
||||||
|
|
@ -30,7 +30,7 @@ module TurniereE2E
|
||||||
def subscribe_timer!(tournament_id:)
|
def subscribe_timer!(tournament_id:)
|
||||||
identifier = {
|
identifier = {
|
||||||
channel: 'TournamentTimersChannel',
|
channel: 'TournamentTimersChannel',
|
||||||
tournament_id: tournament_id
|
tournament_id:
|
||||||
}.to_json
|
}.to_json
|
||||||
|
|
||||||
subscribe!(identifier:) do |message|
|
subscribe!(identifier:) do |message|
|
||||||
|
|
@ -41,8 +41,8 @@ module TurniereE2E
|
||||||
def subscribe_beamer!(tournament_id:, beamer_name:)
|
def subscribe_beamer!(tournament_id:, beamer_name:)
|
||||||
identifier = {
|
identifier = {
|
||||||
channel: 'TournamentBeamersChannel',
|
channel: 'TournamentBeamersChannel',
|
||||||
tournament_id: tournament_id,
|
tournament_id:,
|
||||||
beamer_name: beamer_name
|
beamer_name:
|
||||||
}.to_json
|
}.to_json
|
||||||
|
|
||||||
subscribe!(identifier:) do |message|
|
subscribe!(identifier:) do |message|
|
||||||
|
|
@ -78,31 +78,31 @@ module TurniereE2E
|
||||||
end
|
end
|
||||||
|
|
||||||
def wait_for_payload!(timeout: 20)
|
def wait_for_payload!(timeout: 20)
|
||||||
wait_for!(timeout: timeout) do |message|
|
wait_for!(timeout:) do |message|
|
||||||
team_action_lists_payload_message?(message)
|
team_action_lists_payload_message?(message)
|
||||||
end.fetch('message')
|
end.fetch('message')
|
||||||
end
|
end
|
||||||
|
|
||||||
def wait_for_timer_payload!(timeout: 20)
|
def wait_for_timer_payload!(timeout: 20)
|
||||||
wait_for!(timeout: timeout) do |message|
|
wait_for!(timeout:) do |message|
|
||||||
timer_payload_message?(message)
|
timer_payload_message?(message)
|
||||||
end.fetch('message')
|
end.fetch('message')
|
||||||
end
|
end
|
||||||
|
|
||||||
def wait_for_beamer_payload!(timeout: 20)
|
def wait_for_beamer_payload!(timeout: 20)
|
||||||
wait_for!(timeout: timeout) do |message|
|
wait_for!(timeout:) do |message|
|
||||||
beamer_payload_message?(message)
|
beamer_payload_message?(message)
|
||||||
end.fetch('message')
|
end.fetch('message')
|
||||||
end
|
end
|
||||||
|
|
||||||
def wait_for_tournament_payload!(timeout: 20)
|
def wait_for_tournament_payload!(timeout: 20)
|
||||||
wait_for!(timeout: timeout) do |message|
|
wait_for!(timeout:) do |message|
|
||||||
tournament_payload_message?(message)
|
tournament_payload_message?(message)
|
||||||
end.fetch('message')
|
end.fetch('message')
|
||||||
end
|
end
|
||||||
|
|
||||||
def wait_for_matches_payload!(timeout: 20)
|
def wait_for_matches_payload!(timeout: 20)
|
||||||
wait_for!(timeout: timeout) do |message|
|
wait_for!(timeout:) do |message|
|
||||||
matches_payload_message?(message)
|
matches_payload_message?(message)
|
||||||
end.fetch('message')
|
end.fetch('message')
|
||||||
end
|
end
|
||||||
|
|
@ -135,7 +135,7 @@ module TurniereE2E
|
||||||
driver.text(
|
driver.text(
|
||||||
JSON.dump(
|
JSON.dump(
|
||||||
command: 'subscribe',
|
command: 'subscribe',
|
||||||
identifier: identifier
|
identifier:
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -184,8 +184,8 @@ module TurniereE2E
|
||||||
end
|
end
|
||||||
|
|
||||||
def pump_until_open!(timeout: 10)
|
def pump_until_open!(timeout: 10)
|
||||||
deadline = Time.now + timeout
|
deadline = monotonic_deadline(timeout)
|
||||||
while Time.now < deadline
|
while Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline
|
||||||
read_from_socket(timeout: 0.5)
|
read_from_socket(timeout: 0.5)
|
||||||
return if driver.ready_state == 1
|
return if driver.ready_state == 1
|
||||||
end
|
end
|
||||||
|
|
@ -194,14 +194,14 @@ module TurniereE2E
|
||||||
end
|
end
|
||||||
|
|
||||||
def wait_for!(timeout: 20)
|
def wait_for!(timeout: 20)
|
||||||
deadline = Time.now + timeout
|
deadline = monotonic_deadline(timeout)
|
||||||
loop do
|
loop do
|
||||||
until @messages.empty?
|
until @messages.empty?
|
||||||
message = @messages.pop(true)
|
message = @messages.pop(true)
|
||||||
return message if yield(message)
|
return message if yield(message)
|
||||||
end
|
end
|
||||||
|
|
||||||
raise "websocket message not received within #{timeout}s" if Time.now >= deadline
|
raise "websocket message not received within #{timeout}s" if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
||||||
|
|
||||||
read_from_socket(timeout: 0.5)
|
read_from_socket(timeout: 0.5)
|
||||||
rescue ThreadError
|
rescue ThreadError
|
||||||
|
|
@ -209,6 +209,10 @@ module TurniereE2E
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def monotonic_deadline(timeout)
|
||||||
|
Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
||||||
|
end
|
||||||
|
|
||||||
def read_from_socket(timeout:)
|
def read_from_socket(timeout:)
|
||||||
readable, = IO.select([socket], nil, nil, timeout)
|
readable, = IO.select([socket], nil, nil, timeout)
|
||||||
return if readable.nil?
|
return if readable.nil?
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ module TurniereE2E
|
||||||
attr_reader :base_url
|
attr_reader :base_url
|
||||||
|
|
||||||
def initialize(base_url:)
|
def initialize(base_url:)
|
||||||
@base_url = base_url.sub(%r{/\z}, '')
|
@base_url = base_url.delete_suffix('/')
|
||||||
@auth_headers = {}
|
@auth_headers = {}
|
||||||
@login_credentials = nil
|
@login_credentials = nil
|
||||||
end
|
end
|
||||||
|
|
@ -29,10 +29,10 @@ module TurniereE2E
|
||||||
:post,
|
:post,
|
||||||
'/users',
|
'/users',
|
||||||
body: {
|
body: {
|
||||||
email: email,
|
email:,
|
||||||
password: password,
|
password:,
|
||||||
password_confirmation: password,
|
password_confirmation: password,
|
||||||
username: username
|
username:
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
store_auth_headers!(response)
|
store_auth_headers!(response)
|
||||||
|
|
@ -40,14 +40,14 @@ module TurniereE2E
|
||||||
end
|
end
|
||||||
|
|
||||||
def login!(email:, password:)
|
def login!(email:, password:)
|
||||||
@login_credentials = { email: email, password: password }
|
@login_credentials = { email:, password: }
|
||||||
@auth_headers = {}
|
@auth_headers = {}
|
||||||
response = request(
|
response = request(
|
||||||
:post,
|
:post,
|
||||||
'/users/sign_in',
|
'/users/sign_in',
|
||||||
body: {
|
body: {
|
||||||
email: email,
|
email:,
|
||||||
password: password
|
password:
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
store_auth_headers!(response)
|
store_auth_headers!(response)
|
||||||
|
|
@ -59,19 +59,19 @@ module TurniereE2E
|
||||||
end
|
end
|
||||||
|
|
||||||
def get(path, params: nil, headers: {})
|
def get(path, params: nil, headers: {})
|
||||||
request(:get, path, params: params, headers: headers)
|
request(:get, path, params:, headers:)
|
||||||
end
|
end
|
||||||
|
|
||||||
def post(path, body: nil, params: nil, headers: {})
|
def post(path, body: nil, params: nil, headers: {})
|
||||||
request(:post, path, body: body, params: params, headers: headers)
|
request(:post, path, body:, params:, headers:)
|
||||||
end
|
end
|
||||||
|
|
||||||
def patch(path, body: nil, params: nil, headers: {})
|
def patch(path, body: nil, params: nil, headers: {})
|
||||||
request(:patch, path, body: body, params: params, headers: headers)
|
request(:patch, path, body:, params:, headers:)
|
||||||
end
|
end
|
||||||
|
|
||||||
def delete(path, params: nil, headers: {})
|
def delete(path, params: nil, headers: {})
|
||||||
request(:delete, path, params: params, headers: headers)
|
request(:delete, path, params:, headers:)
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
@ -80,7 +80,7 @@ module TurniereE2E
|
||||||
login!(**@login_credentials) if refresh_session && should_refresh_session?(method, path)
|
login!(**@login_credentials) if refresh_session && should_refresh_session?(method, path)
|
||||||
|
|
||||||
uri = URI.parse("#{base_url}#{path}")
|
uri = URI.parse("#{base_url}#{path}")
|
||||||
uri.query = URI.encode_www_form(params) if params && !params.empty?
|
uri.query = URI.encode_www_form(params) if params&.any?
|
||||||
|
|
||||||
request_class = case method
|
request_class = case method
|
||||||
when :get then Net::HTTP::Get
|
when :get then Net::HTTP::Get
|
||||||
|
|
@ -108,7 +108,8 @@ module TurniereE2E
|
||||||
store_auth_headers!(result)
|
store_auth_headers!(result)
|
||||||
if retry_on_unauthorized && result[:status] == 401 && can_reauthenticate?(path)
|
if retry_on_unauthorized && result[:status] == 401 && can_reauthenticate?(path)
|
||||||
login!(**@login_credentials)
|
login!(**@login_credentials)
|
||||||
return request(method, path, body: body, params: params, headers: headers, retry_on_unauthorized: false, refresh_session: false)
|
return request(method, path, body:, params:, headers:, retry_on_unauthorized: false,
|
||||||
|
refresh_session: false)
|
||||||
end
|
end
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -267,12 +267,8 @@ module TurniereE2E
|
||||||
|
|
||||||
def websocket_failures(level)
|
def websocket_failures(level)
|
||||||
failures = []
|
failures = []
|
||||||
if level.fetch(:errors).positive?
|
failures << "WS #{level.fetch(:clients)} clients: #{level.fetch(:errors)} failed" if level.fetch(:errors).positive?
|
||||||
failures << "WS #{level.fetch(:clients)} clients: #{level.fetch(:errors)} failed"
|
failures << "WS #{level.fetch(:clients)} clients: only #{level.fetch(:opened)} opened" if websocket_open_short?(level)
|
||||||
end
|
|
||||||
if websocket_open_short?(level)
|
|
||||||
failures << "WS #{level.fetch(:clients)} clients: only #{level.fetch(:opened)} opened"
|
|
||||||
end
|
|
||||||
if websocket_metric_short?(level)
|
if websocket_metric_short?(level)
|
||||||
failures << "WS #{level.fetch(:clients)} clients: metrics saw #{level.fetch(:connection_metric)} connections"
|
failures << "WS #{level.fetch(:clients)} clients: metrics saw #{level.fetch(:connection_metric)} connections"
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ require 'uri'
|
||||||
|
|
||||||
module TurniereE2E
|
module TurniereE2E
|
||||||
class ScenarioRunner
|
class ScenarioRunner
|
||||||
DEFAULT_PASSWORD = 'password123'.freeze
|
DEFAULT_PASSWORD = 'password123'
|
||||||
|
|
||||||
attr_reader :client
|
attr_reader :client
|
||||||
|
|
||||||
|
|
@ -36,19 +36,19 @@ module TurniereE2E
|
||||||
timestamp: timer_response.fetch(:timestamp),
|
timestamp: timer_response.fetch(:timestamp),
|
||||||
timer_reason: timer_response.fetch(:timer_reason)
|
timer_reason: timer_response.fetch(:timer_reason)
|
||||||
}
|
}
|
||||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :timer_set
|
return checkpoint_result(session, tournament, checkpoints, stop_at) if stop_at == :timer_set
|
||||||
|
|
||||||
upcoming_matches = get!(
|
upcoming_matches = get!(
|
||||||
"/tournaments/#{tournament.fetch(:id)}/matches",
|
"/tournaments/#{tournament.fetch(:id)}/matches",
|
||||||
params: { state: 'upcoming' }
|
params: { state: 'upcoming' }
|
||||||
)
|
)
|
||||||
checkpoints[:upcoming_group_matches] = { match_ids: upcoming_matches.map { |match| match.fetch(:id) } }
|
checkpoints[:upcoming_group_matches] = { match_ids: upcoming_matches.map { |match| match.fetch(:id) } }
|
||||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :upcoming_group_matches
|
return checkpoint_result(session, tournament, checkpoints, stop_at) if stop_at == :upcoming_group_matches
|
||||||
|
|
||||||
finish_group_stage_matches!(tournament)
|
finish_group_stage_matches!(tournament)
|
||||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||||
checkpoints[:groups_finished] = summarize_tournament(tournament)
|
checkpoints[:groups_finished] = summarize_tournament(tournament)
|
||||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :groups_finished
|
return checkpoint_result(session, tournament, checkpoints, stop_at) if stop_at == :groups_finished
|
||||||
|
|
||||||
statistics = get!("/tournaments/#{tournament.fetch(:id)}/statistics")
|
statistics = get!("/tournaments/#{tournament.fetch(:id)}/statistics")
|
||||||
checkpoints[:statistics] = {
|
checkpoints[:statistics] = {
|
||||||
|
|
@ -60,12 +60,12 @@ module TurniereE2E
|
||||||
finish_stage!(group_stage_of!(tournament).fetch(:id))
|
finish_stage!(group_stage_of!(tournament).fetch(:id))
|
||||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||||
checkpoints[:playoffs_created] = summarize_tournament(tournament)
|
checkpoints[:playoffs_created] = summarize_tournament(tournament)
|
||||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :playoffs_created
|
return checkpoint_result(session, tournament, checkpoints, stop_at) if stop_at == :playoffs_created
|
||||||
|
|
||||||
run_playoffs_until_before_final!(tournament.fetch(:id))
|
run_playoffs_until_before_final!(tournament.fetch(:id))
|
||||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||||
checkpoints[:before_final_finish_attempt] = summarize_tournament(tournament)
|
checkpoints[:before_final_finish_attempt] = summarize_tournament(tournament)
|
||||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :before_final_finish_attempt
|
return checkpoint_result(session, tournament, checkpoints, stop_at) if stop_at == :before_final_finish_attempt
|
||||||
|
|
||||||
finish_attempt = finish_final_stage_matches!(tournament)
|
finish_attempt = finish_final_stage_matches!(tournament)
|
||||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||||
|
|
@ -86,18 +86,27 @@ module TurniereE2E
|
||||||
checkpoints = {
|
checkpoints = {
|
||||||
created: summarize_tournament(tournament)
|
created: summarize_tournament(tournament)
|
||||||
}
|
}
|
||||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :created
|
if stop_at == :created
|
||||||
|
return result(session: session, tournament: tournament, checkpoints: checkpoints,
|
||||||
|
stop_at: stop_at)
|
||||||
|
end
|
||||||
|
|
||||||
finish_group_stage_matches!(tournament)
|
finish_group_stage_matches!(tournament)
|
||||||
finish_stage!(group_stage_of!(fetch_tournament!(tournament.fetch(:id))).fetch(:id))
|
finish_stage!(group_stage_of!(fetch_tournament!(tournament.fetch(:id))).fetch(:id))
|
||||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||||
checkpoints[:playoffs_created] = summarize_tournament(tournament)
|
checkpoints[:playoffs_created] = summarize_tournament(tournament)
|
||||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :playoffs_created
|
if stop_at == :playoffs_created
|
||||||
|
return result(session: session, tournament: tournament, checkpoints: checkpoints,
|
||||||
|
stop_at: stop_at)
|
||||||
|
end
|
||||||
|
|
||||||
run_playoffs_until_before_final!(tournament.fetch(:id))
|
run_playoffs_until_before_final!(tournament.fetch(:id))
|
||||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||||
checkpoints[:before_final_finish_attempt] = summarize_tournament(tournament)
|
checkpoints[:before_final_finish_attempt] = summarize_tournament(tournament)
|
||||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :before_final_finish_attempt
|
if stop_at == :before_final_finish_attempt
|
||||||
|
return result(session: session, tournament: tournament, checkpoints: checkpoints,
|
||||||
|
stop_at: stop_at)
|
||||||
|
end
|
||||||
|
|
||||||
finish_attempt = finish_final_stage_matches!(tournament)
|
finish_attempt = finish_final_stage_matches!(tournament)
|
||||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||||
|
|
@ -132,7 +141,10 @@ module TurniereE2E
|
||||||
checkpoints[created_key] = summarize_tournament(tournament).merge(
|
checkpoints[created_key] = summarize_tournament(tournament).merge(
|
||||||
playoff_matchups: playoff_matchups(tournament)
|
playoff_matchups: playoff_matchups(tournament)
|
||||||
)
|
)
|
||||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == created_key
|
if stop_at == created_key
|
||||||
|
return result(session: session, tournament: tournament, checkpoints: checkpoints,
|
||||||
|
stop_at: stop_at)
|
||||||
|
end
|
||||||
|
|
||||||
next if index == rounds.size - 1
|
next if index == rounds.size - 1
|
||||||
|
|
||||||
|
|
@ -140,7 +152,10 @@ module TurniereE2E
|
||||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||||
deleted_key = deleted_keys.fetch(index)
|
deleted_key = deleted_keys.fetch(index)
|
||||||
checkpoints[deleted_key] = summarize_tournament(tournament)
|
checkpoints[deleted_key] = summarize_tournament(tournament)
|
||||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == deleted_key
|
if stop_at == deleted_key
|
||||||
|
return result(session: session, tournament: tournament, checkpoints: checkpoints,
|
||||||
|
stop_at: stop_at)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at)
|
result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at)
|
||||||
|
|
@ -152,7 +167,10 @@ module TurniereE2E
|
||||||
checkpoints = {
|
checkpoints = {
|
||||||
created: summarize_tournament(tournament)
|
created: summarize_tournament(tournament)
|
||||||
}
|
}
|
||||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :created
|
if stop_at == :created
|
||||||
|
return result(session: session, tournament: tournament, checkpoints: checkpoints,
|
||||||
|
stop_at: stop_at)
|
||||||
|
end
|
||||||
|
|
||||||
run_playoffs_until_complete!(tournament.fetch(:id))
|
run_playoffs_until_complete!(tournament.fetch(:id))
|
||||||
tournament = fetch_tournament!(tournament.fetch(:id))
|
tournament = fetch_tournament!(tournament.fetch(:id))
|
||||||
|
|
@ -170,7 +188,10 @@ module TurniereE2E
|
||||||
checkpoints = {
|
checkpoints = {
|
||||||
created: summarize_tournament(tournament)
|
created: summarize_tournament(tournament)
|
||||||
}
|
}
|
||||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :created
|
if stop_at == :created
|
||||||
|
return result(session: session, tournament: tournament, checkpoints: checkpoints,
|
||||||
|
stop_at: stop_at)
|
||||||
|
end
|
||||||
|
|
||||||
group = group_stage_of!(tournament).fetch(:groups).first
|
group = group_stage_of!(tournament).fetch(:groups).first
|
||||||
play_group_matches_with_explicit_scores!(group, group_direct_comparison_score_plan(case_name))
|
play_group_matches_with_explicit_scores!(group, group_direct_comparison_score_plan(case_name))
|
||||||
|
|
@ -250,7 +271,10 @@ module TurniereE2E
|
||||||
checkpoints = {
|
checkpoints = {
|
||||||
created: summarize_tournament(tournament)
|
created: summarize_tournament(tournament)
|
||||||
}
|
}
|
||||||
return result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at) if stop_at == :created
|
if stop_at == :created
|
||||||
|
return result(session: session, tournament: tournament, checkpoints: checkpoints,
|
||||||
|
stop_at: stop_at)
|
||||||
|
end
|
||||||
|
|
||||||
profiled_show = fetch_tournament_profile!(tournament.fetch(:id))
|
profiled_show = fetch_tournament_profile!(tournament.fetch(:id))
|
||||||
checkpoints[:profiled_show] = profiled_show
|
checkpoints[:profiled_show] = profiled_show
|
||||||
|
|
@ -259,7 +283,7 @@ module TurniereE2E
|
||||||
end
|
end
|
||||||
|
|
||||||
def wait_for_healthcheck!(timeout_seconds: 30)
|
def wait_for_healthcheck!(timeout_seconds: 30)
|
||||||
started_at = Time.now
|
started_at = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||||
loop do
|
loop do
|
||||||
begin
|
begin
|
||||||
response = client.get('/healthz')
|
response = client.get('/healthz')
|
||||||
|
|
@ -268,7 +292,8 @@ module TurniereE2E
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
raise "healthcheck did not become ready within #{timeout_seconds}s" if Time.now - started_at > timeout_seconds
|
elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at
|
||||||
|
raise "healthcheck did not become ready within #{timeout_seconds}s" if elapsed > timeout_seconds
|
||||||
|
|
||||||
sleep 0.5
|
sleep 0.5
|
||||||
end
|
end
|
||||||
|
|
@ -282,7 +307,7 @@ module TurniereE2E
|
||||||
unique = SecureRandom.hex(6)
|
unique = SecureRandom.hex(6)
|
||||||
email = "e2e-#{unique}@example.com"
|
email = "e2e-#{unique}@example.com"
|
||||||
username = "e2e-#{unique}"
|
username = "e2e-#{unique}"
|
||||||
response = client.register!(email: email, password: DEFAULT_PASSWORD, username: username)
|
client.register!(email: email, password: DEFAULT_PASSWORD, username: username)
|
||||||
return { email: email, username: username } if client.authenticated?
|
return { email: email, username: username } if client.authenticated?
|
||||||
|
|
||||||
login_response = client.login!(email: email, password: DEFAULT_PASSWORD)
|
login_response = client.login!(email: email, password: DEFAULT_PASSWORD)
|
||||||
|
|
@ -350,9 +375,12 @@ module TurniereE2E
|
||||||
def run_playoffs_until_before_final!(tournament_id)
|
def run_playoffs_until_before_final!(tournament_id)
|
||||||
loop do
|
loop do
|
||||||
tournament = fetch_tournament!(tournament_id)
|
tournament = fetch_tournament!(tournament_id)
|
||||||
playable_matches = playoff_matches(tournament).reject { |match| match.fetch(:stage_level) == 0 }
|
playable_matches = playoff_matches(tournament).reject { |match| match.fetch(:stage_level).zero? }
|
||||||
.select { |match| match.fetch(:state) == 'not_started' }
|
.select { |match| match.fetch(:state) == 'not_started' }
|
||||||
.sort_by { |match| [-match.fetch(:stage_level), match.fetch(:position)] }
|
.sort_by do |match|
|
||||||
|
[-match.fetch(:stage_level),
|
||||||
|
match.fetch(:position)]
|
||||||
|
end
|
||||||
|
|
||||||
break if playable_matches.empty?
|
break if playable_matches.empty?
|
||||||
|
|
||||||
|
|
@ -366,7 +394,10 @@ module TurniereE2E
|
||||||
loop do
|
loop do
|
||||||
tournament = fetch_tournament!(tournament_id)
|
tournament = fetch_tournament!(tournament_id)
|
||||||
playable_matches = playoff_matches(tournament).select { |match| match.fetch(:state) == 'not_started' }
|
playable_matches = playoff_matches(tournament).select { |match| match.fetch(:state) == 'not_started' }
|
||||||
.sort_by { |match| [-match.fetch(:stage_level), match.fetch(:position)] }
|
.sort_by do |match|
|
||||||
|
[-match.fetch(:stage_level),
|
||||||
|
match.fetch(:position)]
|
||||||
|
end
|
||||||
break if playable_matches.empty?
|
break if playable_matches.empty?
|
||||||
|
|
||||||
playable_matches.each do |match|
|
playable_matches.each do |match|
|
||||||
|
|
@ -390,7 +421,9 @@ module TurniereE2E
|
||||||
expect_status!(finished, expected_finish_status, "finish match #{match.fetch(:id)}")
|
expect_status!(finished, expected_finish_status, "finish match #{match.fetch(:id)}")
|
||||||
if expected_finish_error
|
if expected_finish_error
|
||||||
actual_error = finished.fetch(:json).fetch(:error)
|
actual_error = finished.fetch(:json).fetch(:error)
|
||||||
raise "expected finish error #{expected_finish_error.inspect}, got #{actual_error.inspect}" unless actual_error == expected_finish_error
|
unless actual_error == expected_finish_error
|
||||||
|
raise "expected finish error #{expected_finish_error.inspect}, got #{actual_error.inspect}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
@ -418,7 +451,9 @@ module TurniereE2E
|
||||||
expect_status!(finished, expected_finish_status, "finish match #{match.fetch(:id)}")
|
expect_status!(finished, expected_finish_status, "finish match #{match.fetch(:id)}")
|
||||||
if expected_finish_error
|
if expected_finish_error
|
||||||
actual_error = finished.fetch(:json).fetch(:error)
|
actual_error = finished.fetch(:json).fetch(:error)
|
||||||
raise "expected finish error #{expected_finish_error.inspect}, got #{actual_error.inspect}" unless actual_error == expected_finish_error
|
unless actual_error == expected_finish_error
|
||||||
|
raise "expected finish error #{expected_finish_error.inspect}, got #{actual_error.inspect}"
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
{
|
{
|
||||||
|
|
@ -481,7 +516,9 @@ module TurniereE2E
|
||||||
end
|
end
|
||||||
|
|
||||||
def final_stage_matches_of!(tournament)
|
def final_stage_matches_of!(tournament)
|
||||||
final_stage = tournament.fetch(:stages).find { |stage| stage.fetch(:level) == 0 } || raise('final stage not found')
|
final_stage = tournament.fetch(:stages).find do |stage|
|
||||||
|
stage.fetch(:level).zero?
|
||||||
|
end || raise('final stage not found')
|
||||||
attach_stage_level(final_stage).fetch(:matches).sort_by { |match| match.fetch(:position) }
|
attach_stage_level(final_stage).fetch(:matches).sort_by { |match| match.fetch(:position) }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -496,7 +533,9 @@ module TurniereE2E
|
||||||
|
|
||||||
{
|
{
|
||||||
status: 200,
|
status: 200,
|
||||||
finished_match_ids: final_stage_matches_of!(fetch_tournament!(tournament.fetch(:id))).map { |match| match.fetch(:id) }
|
finished_match_ids: final_stage_matches_of!(fetch_tournament!(tournament.fetch(:id))).map do |match|
|
||||||
|
match.fetch(:id)
|
||||||
|
end
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -574,7 +613,7 @@ module TurniereE2E
|
||||||
|
|
||||||
def create_group_stage_payload(group_count:, teams_per_group:, playoff_teams_amount:)
|
def create_group_stage_payload(group_count:, teams_per_group:, playoff_teams_amount:)
|
||||||
teams = group_count.times.flat_map do |group_index|
|
teams = group_count.times.flat_map do |group_index|
|
||||||
teams_per_group.times.map do |seed_index|
|
Array.new(teams_per_group) do |seed_index|
|
||||||
{
|
{
|
||||||
name: "G#{group_index + 1}-S#{seed_index + 1}",
|
name: "G#{group_index + 1}-S#{seed_index + 1}",
|
||||||
group: group_index
|
group: group_index
|
||||||
|
|
@ -626,7 +665,7 @@ module TurniereE2E
|
||||||
name: "HTTP E2E Playoff Only #{team_count} #{SecureRandom.hex(3)}",
|
name: "HTTP E2E Playoff Only #{team_count} #{SecureRandom.hex(3)}",
|
||||||
description: 'HTTP lifecycle E2E tournament',
|
description: 'HTTP lifecycle E2E tournament',
|
||||||
public: false,
|
public: false,
|
||||||
teams: team_count.times.map { |index| { name: "G0-S#{index + 1}" } }
|
teams: Array.new(team_count) { |index| { name: "G0-S#{index + 1}" } }
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -739,7 +778,7 @@ module TurniereE2E
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse_server_timing(header)
|
def parse_server_timing(header)
|
||||||
return {} if header.nil? || header.empty?
|
return {} if header.to_s.empty?
|
||||||
|
|
||||||
header.split(',').each_with_object({}) do |entry, sections|
|
header.split(',').each_with_object({}) do |entry, sections|
|
||||||
name, duration = entry.strip.match(/\A([^;]+);dur=([0-9.]+)\z/)&.captures
|
name, duration = entry.strip.match(/\A([^;]+);dur=([0-9.]+)\z/)&.captures
|
||||||
|
|
@ -758,6 +797,10 @@ module TurniereE2E
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def checkpoint_result(session, tournament, checkpoints, stop_at)
|
||||||
|
result(session: session, tournament: tournament, checkpoints: checkpoints, stop_at: stop_at)
|
||||||
|
end
|
||||||
|
|
||||||
def expect_status!(response, expected_status, action)
|
def expect_status!(response, expected_status, action)
|
||||||
return if response.fetch(:status) == expected_status
|
return if response.fetch(:status) == expected_status
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -73,32 +73,31 @@ result = runner.run
|
||||||
|
|
||||||
if options[:format] == 'json'
|
if options[:format] == 'json'
|
||||||
puts JSON.pretty_generate(result)
|
puts JSON.pretty_generate(result)
|
||||||
exit
|
else
|
||||||
end
|
puts "Load test base_url=#{result.fetch(:base_url)} tournament_id=#{result.dig(:tournament, :id)}"
|
||||||
|
|
||||||
puts "Load test base_url=#{result.fetch(:base_url)} tournament_id=#{result.dig(:tournament, :id)}"
|
result.fetch(:http).each do |level|
|
||||||
|
|
||||||
result.fetch(:http).each do |level|
|
|
||||||
puts(
|
puts(
|
||||||
"HTTP #{level.fetch(:clients)} clients: " \
|
"HTTP #{level.fetch(:clients)} clients: " \
|
||||||
"#{level.fetch(:ok)}/#{level.fetch(:requests)} ok, errors=#{level.fetch(:errors)}, " \
|
"#{level.fetch(:ok)}/#{level.fetch(:requests)} ok, errors=#{level.fetch(:errors)}, " \
|
||||||
"p50=#{level.fetch(:p50_ms)}ms, p95=#{level.fetch(:p95_ms)}ms, " \
|
"p50=#{level.fetch(:p50_ms)}ms, p95=#{level.fetch(:p95_ms)}ms, " \
|
||||||
"max=#{level.fetch(:max_ms)}ms, rps=#{level.fetch(:requests_per_second)}"
|
"max=#{level.fetch(:max_ms)}ms, rps=#{level.fetch(:requests_per_second)}"
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
result.fetch(:websocket).each do |level|
|
result.fetch(:websocket).each do |level|
|
||||||
puts(
|
puts(
|
||||||
"WS #{level.fetch(:clients)} clients: " \
|
"WS #{level.fetch(:clients)} clients: " \
|
||||||
"opened=#{level.fetch(:opened)}, errors=#{level.fetch(:errors)}, " \
|
"opened=#{level.fetch(:opened)}, errors=#{level.fetch(:errors)}, " \
|
||||||
"p50_open=#{level.fetch(:p50_open_ms)}ms, p95_open=#{level.fetch(:p95_open_ms)}ms, " \
|
"p50_open=#{level.fetch(:p50_open_ms)}ms, p95_open=#{level.fetch(:p95_open_ms)}ms, " \
|
||||||
"connections_metric=#{level.fetch(:connection_metric)}"
|
"connections_metric=#{level.fetch(:connection_metric)}"
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
metrics = result.fetch(:metrics)
|
metrics = result.fetch(:metrics)
|
||||||
puts(
|
puts(
|
||||||
"Metrics: action_cable_connections=#{metrics.fetch(:action_cable_connections)}, " \
|
"Metrics: action_cable_connections=#{metrics.fetch(:action_cable_connections)}, " \
|
||||||
"action_cable_connections_total=#{metrics.fetch(:action_cable_connections_total)}, " \
|
"action_cable_connections_total=#{metrics.fetch(:action_cable_connections_total)}, " \
|
||||||
"ruby_threads=#{metrics.fetch(:ruby_threads)}"
|
"ruby_threads=#{metrics.fetch(:ruby_threads)}"
|
||||||
)
|
)
|
||||||
|
end
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ module Local
|
||||||
result = yield
|
result = yield
|
||||||
end
|
end
|
||||||
duration = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000.0
|
duration = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - started_at) * 1000.0
|
||||||
@measurements << { name: name, duration: duration.round(1), query_count: query_count }
|
@measurements << { name:, duration: duration.round(1), query_count: }
|
||||||
result
|
result
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,11 @@ require 'json'
|
||||||
require 'optparse'
|
require 'optparse'
|
||||||
require_relative '../e2e/lib/scenario_runner'
|
require_relative '../e2e/lib/scenario_runner'
|
||||||
|
|
||||||
|
def fail_with(message)
|
||||||
|
warn message
|
||||||
|
raise SystemExit, 1
|
||||||
|
end
|
||||||
|
|
||||||
options = {
|
options = {
|
||||||
base_url: ENV.fetch('TURNIERE_E2E_BASE_URL', 'http://127.0.0.1:3000'),
|
base_url: ENV.fetch('TURNIERE_E2E_BASE_URL', 'http://127.0.0.1:3000'),
|
||||||
stop_at: nil,
|
stop_at: nil,
|
||||||
|
|
@ -12,9 +17,9 @@ options = {
|
||||||
group_count: 4,
|
group_count: 4,
|
||||||
teams_per_group: 4,
|
teams_per_group: 4,
|
||||||
playoff_teams_amount: 8,
|
playoff_teams_amount: 8,
|
||||||
email: ENV['TURNIERE_E2E_EMAIL'],
|
email: ENV.fetch('TURNIERE_E2E_EMAIL', nil),
|
||||||
password: ENV['TURNIERE_E2E_PASSWORD'],
|
password: ENV.fetch('TURNIERE_E2E_PASSWORD', nil),
|
||||||
username: ENV['TURNIERE_E2E_USERNAME']
|
username: ENV.fetch('TURNIERE_E2E_USERNAME', nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
OptionParser.new do |parser|
|
OptionParser.new do |parser|
|
||||||
|
|
@ -22,9 +27,15 @@ OptionParser.new do |parser|
|
||||||
|
|
||||||
parser.on('--base-url URL', 'API base URL') { |value| options[:base_url] = value }
|
parser.on('--base-url URL', 'API base URL') { |value| options[:base_url] = value }
|
||||||
parser.on('--stop-at CHECKPOINT', 'Stop scenario at a named checkpoint') { |value| options[:stop_at] = value.to_sym }
|
parser.on('--stop-at CHECKPOINT', 'Stop scenario at a named checkpoint') { |value| options[:stop_at] = value.to_sym }
|
||||||
parser.on('--team-count COUNT', Integer, 'Team count for playoff_only scenario') { |value| options[:team_count] = value }
|
parser.on('--team-count COUNT', Integer, 'Team count for playoff_only scenario') do |value|
|
||||||
parser.on('--group-count COUNT', Integer, 'Group count for group-stage scenarios') { |value| options[:group_count] = value }
|
options[:team_count] = value
|
||||||
parser.on('--teams-per-group COUNT', Integer, 'Teams per group for group-stage scenarios') { |value| options[:teams_per_group] = value }
|
end
|
||||||
|
parser.on('--group-count COUNT', Integer, 'Group count for group-stage scenarios') do |value|
|
||||||
|
options[:group_count] = value
|
||||||
|
end
|
||||||
|
parser.on('--teams-per-group COUNT', Integer, 'Teams per group for group-stage scenarios') do |value|
|
||||||
|
options[:teams_per_group] = value
|
||||||
|
end
|
||||||
parser.on('--playoff-teams-amount COUNT', Integer, 'Playoff teams amount for group-stage scenarios') do |value|
|
parser.on('--playoff-teams-amount COUNT', Integer, 'Playoff teams amount for group-stage scenarios') do |value|
|
||||||
options[:playoff_teams_amount] = value
|
options[:playoff_teams_amount] = value
|
||||||
end
|
end
|
||||||
|
|
@ -33,7 +44,9 @@ OptionParser.new do |parser|
|
||||||
parser.on('--username USERNAME', 'Confirmed E2E user username') { |value| options[:username] = value }
|
parser.on('--username USERNAME', 'Confirmed E2E user username') { |value| options[:username] = value }
|
||||||
end.parse!
|
end.parse!
|
||||||
|
|
||||||
scenario = ARGV.shift or abort('Missing scenario name')
|
scenario = ARGV.shift
|
||||||
|
fail_with('Missing scenario name') if scenario.nil?
|
||||||
|
|
||||||
runner = TurniereE2E::ScenarioRunner.new(
|
runner = TurniereE2E::ScenarioRunner.new(
|
||||||
base_url: options[:base_url],
|
base_url: options[:base_url],
|
||||||
email: options[:email],
|
email: options[:email],
|
||||||
|
|
@ -58,7 +71,7 @@ result = case scenario
|
||||||
stop_at: options[:stop_at] || :profiled_show
|
stop_at: options[:stop_at] || :profiled_show
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
abort("Unknown scenario #{scenario.inspect}")
|
fail_with("Unknown scenario #{scenario.inspect}")
|
||||||
end
|
end
|
||||||
|
|
||||||
puts JSON.pretty_generate(result)
|
puts JSON.pretty_generate(result)
|
||||||
|
|
|
||||||
14
tasks.py
14
tasks.py
|
|
@ -57,6 +57,18 @@ BLACKBOX_MAILGUN_API_KEY = "blackbox-test-api-key"
|
||||||
BLACKBOX_MAILGUN_DOMAIN = "blackbox.example.com"
|
BLACKBOX_MAILGUN_DOMAIN = "blackbox.example.com"
|
||||||
SPEC_ROOT = Path("spec")
|
SPEC_ROOT = Path("spec")
|
||||||
E2E_SPEC_ROOT = SPEC_ROOT / "e2e" / "http"
|
E2E_SPEC_ROOT = SPEC_ROOT / "e2e" / "http"
|
||||||
|
LINT_TARGETS = (
|
||||||
|
"Gemfile",
|
||||||
|
"Rakefile",
|
||||||
|
"config.ru",
|
||||||
|
"app",
|
||||||
|
"config",
|
||||||
|
"db/migrate",
|
||||||
|
"db/seeds.rb",
|
||||||
|
"e2e",
|
||||||
|
"lib",
|
||||||
|
"script",
|
||||||
|
)
|
||||||
ROOT = Path(__file__).resolve().parent
|
ROOT = Path(__file__).resolve().parent
|
||||||
DB_DIR = ROOT / "db"
|
DB_DIR = ROOT / "db"
|
||||||
GEMFILE_LOCK_PATH = ROOT / "Gemfile.lock"
|
GEMFILE_LOCK_PATH = ROOT / "Gemfile.lock"
|
||||||
|
|
@ -802,7 +814,7 @@ def test_shard(c, node_index=1, node_total=8, dry_run=False):
|
||||||
@task
|
@task
|
||||||
def lint(c):
|
def lint(c):
|
||||||
"""Run RuboCop."""
|
"""Run RuboCop."""
|
||||||
c.run(_bundle_exec("rubocop"), pty=True)
|
c.run(_bundle_exec("rubocop", *LINT_TARGETS, "--cache", "false"), pty=True)
|
||||||
|
|
||||||
|
|
||||||
@task(name="bootstrap-dev-user")
|
@task(name="bootstrap-dev-user")
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue